001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 *
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 *
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017package org.apache.commons.imaging.formats.webp;
018
019import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
020import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
021import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes;
022
023import java.awt.Dimension;
024import java.awt.image.BufferedImage;
025import java.io.Closeable;
026import java.io.IOException;
027import java.io.InputStream;
028import java.io.PrintWriter;
029import java.nio.ByteOrder;
030import java.util.ArrayList;
031
032import org.apache.commons.imaging.AbstractImageParser;
033import org.apache.commons.imaging.ImageFormat;
034import org.apache.commons.imaging.ImageFormats;
035import org.apache.commons.imaging.ImageInfo;
036import org.apache.commons.imaging.ImagingException;
037import org.apache.commons.imaging.bytesource.ByteSource;
038import org.apache.commons.imaging.common.XmpEmbeddable;
039import org.apache.commons.imaging.common.XmpImagingParameters;
040import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
041import org.apache.commons.imaging.formats.tiff.TiffImageParser;
042import org.apache.commons.imaging.formats.webp.chunks.WebPChunk;
043import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8;
044import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8l;
045import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8x;
046import org.apache.commons.imaging.formats.webp.chunks.WebPChunkXml;
047import org.apache.commons.imaging.internal.SafeOperations;
048
049/**
050 * WebP image parser.
051 *
052 * @since 1.0-alpha4
053 */
054public class WebPImageParser extends AbstractImageParser<WebPImagingParameters> implements XmpEmbeddable<WebPImagingParameters> {
055
056    private static final class ChunksReader implements Closeable {
057        private final InputStream is;
058        private final WebPChunkType[] chunkTypes;
059        private int sizeCount = 4;
060        private boolean firstChunk = true;
061
062        final int fileSize;
063
064        ChunksReader(final ByteSource byteSource) throws IOException, ImagingException {
065            this(byteSource, (WebPChunkType[]) null);
066        }
067
068        ChunksReader(final ByteSource byteSource, final WebPChunkType... chunkTypes) throws ImagingException, IOException {
069            this.is = byteSource.getInputStream();
070            this.chunkTypes = chunkTypes;
071            this.fileSize = readFileHeader(is);
072        }
073
074        @Override
075        public void close() throws IOException {
076            is.close();
077        }
078
079        int getOffset() {
080            return SafeOperations.add(sizeCount, 8); // File Header
081        }
082
083        WebPChunk readChunk() throws ImagingException, IOException {
084            while (sizeCount < fileSize) {
085                final int type = read4Bytes("Chunk Type", is, "Not a valid WebP file", ByteOrder.LITTLE_ENDIAN);
086                final int payloadSize = read4Bytes("Chunk Size", is, "Not a valid WebP file", ByteOrder.LITTLE_ENDIAN);
087                if (payloadSize < 0) {
088                    throw new ImagingException("Chunk Payload is too long:" + payloadSize);
089                }
090                final boolean padding = payloadSize % 2 != 0;
091                final int chunkSize = SafeOperations.add(8, padding ? 1 : 0, payloadSize);
092
093                if (firstChunk) {
094                    firstChunk = false;
095                    if (type != WebPChunkType.VP8.value && type != WebPChunkType.VP8L.value && type != WebPChunkType.VP8X.value) {
096                        throw new ImagingException("First Chunk must be VP8, VP8L or VP8X");
097                    }
098                }
099
100                if (chunkTypes != null) {
101                    boolean skip = true;
102                    for (final WebPChunkType t : chunkTypes) {
103                        if (t.value == type) {
104                            skip = false;
105                            break;
106                        }
107                    }
108                    if (skip) {
109                        skipBytes(is, payloadSize + (padding ? 1 : 0));
110                        sizeCount = SafeOperations.add(sizeCount, chunkSize);
111                        continue;
112                    }
113                }
114
115                final byte[] bytes = readBytes("Chunk Payload", is, payloadSize);
116                final WebPChunk chunk = WebPChunkType.makeChunk(type, payloadSize, bytes);
117                if (padding) {
118                    skipBytes(is, 1);
119                }
120
121                sizeCount = SafeOperations.add(sizeCount, chunkSize);
122                return chunk;
123            }
124
125            if (firstChunk) {
126                throw new ImagingException("No WebP chunks found");
127            }
128            return null;
129        }
130    }
131
132    private static final String DEFAULT_EXTENSION = ImageFormats.WEBP.getDefaultExtension();
133
134    private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.WEBP.getExtensions();
135
136    /**
137     * Read the file header of WebP file.
138     *
139     * @return file size in file header (including the WebP signature, excluding the TIFF signature and the file size field).
140     */
141    private static int readFileHeader(final InputStream is) throws IOException, ImagingException {
142        final byte[] buffer = new byte[4];
143        if (is.read(buffer) < 4 || !WebPConstants.RIFF_SIGNATURE.equals(buffer)) {
144            throw new ImagingException("Not a valid WebP file");
145        }
146
147        final int fileSize = read4Bytes("File Size", is, "Not a valid WebP file", ByteOrder.LITTLE_ENDIAN);
148        if (fileSize < 0) {
149            throw new ImagingException("File size is too long:" + fileSize);
150        }
151
152        if (is.read(buffer) < 4 || !WebPConstants.WEBP_SIGNATURE.equals(buffer)) {
153            throw new ImagingException("Not a valid WebP file");
154        }
155
156        return fileSize;
157    }
158
159    @Override
160    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
161        pw.println("webp.dumpImageFile");
162        try (ChunksReader reader = new ChunksReader(byteSource)) {
163            int offset = reader.getOffset();
164            WebPChunk chunk = reader.readChunk();
165            if (chunk == null) {
166                throw new ImagingException("No WebP chunks found");
167            }
168
169            // TODO: this does not look too risky; a user could craft an image
170            // with millions of chunks, that are really expensive to dump,
171            // but that should result in a large image, where we can short-
172            // -circuit the operation somewhere else - if needed.
173            do {
174                chunk.dump(pw, offset);
175
176                offset = reader.getOffset();
177                chunk = reader.readChunk();
178            } while (chunk != null);
179        }
180        return true;
181    }
182
183    @Override
184    protected String[] getAcceptedExtensions() {
185        return ACCEPTED_EXTENSIONS;
186    }
187
188    @Override
189    protected ImageFormat[] getAcceptedTypes() {
190        return new ImageFormat[] { ImageFormats.WEBP };
191    }
192
193    @Override
194    public BufferedImage getBufferedImage(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException {
195        throw new ImagingException("Reading WebP files is currently not supported");
196    }
197
198    @Override
199    public String getDefaultExtension() {
200        return DEFAULT_EXTENSION;
201    }
202
203    @Override
204    public WebPImagingParameters getDefaultParameters() {
205        return new WebPImagingParameters();
206    }
207
208    @Override
209    public byte[] getIccProfileBytes(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException {
210        try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.ICCP)) {
211            final WebPChunk chunk = reader.readChunk();
212            return chunk == null ? null : chunk.getBytes();
213        }
214    }
215
216    @Override
217    public ImageInfo getImageInfo(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException {
218        try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.VP8, WebPChunkType.VP8L, WebPChunkType.VP8X, WebPChunkType.ANMF)) {
219            String formatDetails;
220            int width;
221            int height;
222            int numberOfImages;
223            boolean hasAlpha = false;
224            ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB;
225
226            WebPChunk chunk = reader.readChunk();
227            if (chunk instanceof WebPChunkVp8) {
228                formatDetails = "WebP/Lossy";
229                numberOfImages = 1;
230
231                final WebPChunkVp8 vp8 = (WebPChunkVp8) chunk;
232                width = vp8.getWidth();
233                height = vp8.getHeight();
234                colorType = ImageInfo.ColorType.YCbCr;
235            } else if (chunk instanceof WebPChunkVp8l) {
236                formatDetails = "WebP/Lossless";
237                numberOfImages = 1;
238
239                final WebPChunkVp8l vp8l = (WebPChunkVp8l) chunk;
240                width = vp8l.getImageWidth();
241                height = vp8l.getImageHeight();
242            } else if (chunk instanceof WebPChunkVp8x) {
243                final WebPChunkVp8x vp8x = (WebPChunkVp8x) chunk;
244                width = vp8x.getCanvasWidth();
245                height = vp8x.getCanvasHeight();
246                hasAlpha = ((WebPChunkVp8x) chunk).hasAlpha();
247
248                if (vp8x.hasAnimation()) {
249                    formatDetails = "WebP/Animation";
250
251                    numberOfImages = 0;
252                    while ((chunk = reader.readChunk()) != null) {
253                        if (chunk.getType() == WebPChunkType.ANMF.value) {
254                            numberOfImages++;
255                        }
256                    }
257
258                } else {
259                    numberOfImages = 1;
260                    chunk = reader.readChunk();
261
262                    if (chunk == null) {
263                        throw new ImagingException("Image has no content");
264                    }
265
266                    if (chunk.getType() == WebPChunkType.ANMF.value) {
267                        throw new ImagingException("Non animated image should not contain ANMF chunks");
268                    }
269
270                    if (chunk.getType() == WebPChunkType.VP8.value) {
271                        formatDetails = "WebP/Lossy (Extended)";
272                        colorType = ImageInfo.ColorType.YCbCr;
273                    } else if (chunk.getType() == WebPChunkType.VP8L.value) {
274                        formatDetails = "WebP/Lossless (Extended)";
275                    } else {
276                        throw new ImagingException("Unknown WebP chunk type: " + chunk);
277                    }
278                }
279            } else {
280                throw new ImagingException("Unknown WebP chunk type: " + chunk);
281            }
282
283            return new ImageInfo(formatDetails, 32, new ArrayList<>(), ImageFormats.WEBP, "webp", height, "image/webp", numberOfImages, -1, -1, -1, -1, width,
284                    false, hasAlpha, false, colorType, ImageInfo.CompressionAlgorithm.UNKNOWN);
285        }
286    }
287
288    @Override
289    public Dimension getImageSize(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException {
290        try (ChunksReader reader = new ChunksReader(byteSource)) {
291            final WebPChunk chunk = reader.readChunk();
292            if (chunk instanceof WebPChunkVp8) {
293                final WebPChunkVp8 vp8 = (WebPChunkVp8) chunk;
294                return new Dimension(vp8.getWidth(), vp8.getHeight());
295            }
296            if (chunk instanceof WebPChunkVp8l) {
297                final WebPChunkVp8l vp8l = (WebPChunkVp8l) chunk;
298                return new Dimension(vp8l.getImageWidth(), vp8l.getImageHeight());
299            }
300            if (chunk instanceof WebPChunkVp8x) {
301                final WebPChunkVp8x vp8x = (WebPChunkVp8x) chunk;
302                return new Dimension(vp8x.getCanvasWidth(), vp8x.getCanvasHeight());
303            }
304            throw new ImagingException("Unknown WebP chunk type: " + chunk);
305        }
306    }
307
308    @Override
309    public WebPImageMetadata getMetadata(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException {
310        try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.EXIF)) {
311            final WebPChunk chunk = reader.readChunk();
312            return chunk == null ? null : new WebPImageMetadata((TiffImageMetadata) new TiffImageParser().getMetadata(chunk.getBytes()));
313        }
314    }
315
316    @Override
317    public String getName() {
318        return "WebP-Custom";
319    }
320
321    @Override
322    public String getXmpXml(final ByteSource byteSource, final XmpImagingParameters<WebPImagingParameters> params) throws ImagingException, IOException {
323        try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.XMP)) {
324            final WebPChunkXml chunk = (WebPChunkXml) reader.readChunk();
325            return chunk == null ? null : chunk.getXml();
326        }
327    }
328}