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.AbstractWebPChunk;
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.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        AbstractWebPChunk 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 AbstractWebPChunk chunk = WebPChunkType.makeChunk(type, payloadSize, bytes);
117                if (padding) {
118                    skipBytes(is, 1);
119                }
120
121                sizeCount = SafeOperations.add(sizeCount, chunkSize);
122                return chunk; // NOPMD How can we do this better?
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     * Reads 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    /**
160     * Constructs a new instance with the big-endian byte order.
161     */
162    public WebPImageParser() {
163        // empty
164    }
165
166    @Override
167    public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
168        pw.println("webp.dumpImageFile");
169        try (ChunksReader reader = new ChunksReader(byteSource)) {
170            int offset = reader.getOffset();
171            AbstractWebPChunk chunk = reader.readChunk();
172            if (chunk == null) {
173                throw new ImagingException("No WebP chunks found");
174            }
175
176            // TODO: this does not look too risky; a user could craft an image
177            // with millions of chunks, that are really expensive to dump,
178            // but that should result in a large image, where we can short-
179            // -circuit the operation somewhere else - if needed.
180            do {
181                chunk.dump(pw, offset);
182
183                offset = reader.getOffset();
184                chunk = reader.readChunk();
185            } while (chunk != null);
186        }
187        return true;
188    }
189
190    @Override
191    protected String[] getAcceptedExtensions() {
192        return ACCEPTED_EXTENSIONS;
193    }
194
195    @Override
196    protected ImageFormat[] getAcceptedTypes() {
197        return new ImageFormat[] { ImageFormats.WEBP };
198    }
199
200    @Override
201    public BufferedImage getBufferedImage(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException {
202        throw new ImagingException("Reading WebP files is currently not supported");
203    }
204
205    @Override
206    public String getDefaultExtension() {
207        return DEFAULT_EXTENSION;
208    }
209
210    @Override
211    public WebPImagingParameters getDefaultParameters() {
212        return new WebPImagingParameters();
213    }
214
215    @Override
216    public byte[] getIccProfileBytes(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException {
217        try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.ICCP)) {
218            final AbstractWebPChunk chunk = reader.readChunk();
219            return chunk == null ? null : chunk.getBytes();
220        }
221    }
222
223    @Override
224    public ImageInfo getImageInfo(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException {
225        try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.VP8, WebPChunkType.VP8L, WebPChunkType.VP8X, WebPChunkType.ANMF)) {
226            final String formatDetails;
227            final int width;
228            final int height;
229            int numberOfImages;
230            boolean hasAlpha = false;
231            ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB;
232
233            AbstractWebPChunk chunk = reader.readChunk();
234            if (chunk instanceof WebPChunkVp8) {
235                formatDetails = "WebP/Lossy";
236                numberOfImages = 1;
237
238                final WebPChunkVp8 vp8 = (WebPChunkVp8) chunk;
239                width = vp8.getWidth();
240                height = vp8.getHeight();
241                colorType = ImageInfo.ColorType.YCbCr;
242            } else if (chunk instanceof WebPChunkVp8l) {
243                formatDetails = "WebP/Lossless";
244                numberOfImages = 1;
245
246                final WebPChunkVp8l vp8l = (WebPChunkVp8l) chunk;
247                width = vp8l.getImageWidth();
248                height = vp8l.getImageHeight();
249            } else if (chunk instanceof WebPChunkVp8x) {
250                final WebPChunkVp8x vp8x = (WebPChunkVp8x) chunk;
251                width = vp8x.getCanvasWidth();
252                height = vp8x.getCanvasHeight();
253                hasAlpha = ((WebPChunkVp8x) chunk).hasAlpha();
254
255                if (vp8x.hasAnimation()) {
256                    formatDetails = "WebP/Animation";
257
258                    numberOfImages = 0;
259                    while ((chunk = reader.readChunk()) != null) {
260                        if (chunk.getType() == WebPChunkType.ANMF.value) {
261                            numberOfImages++;
262                        }
263                    }
264
265                } else {
266                    numberOfImages = 1;
267                    chunk = reader.readChunk();
268
269                    if (chunk == null) {
270                        throw new ImagingException("Image has no content");
271                    }
272
273                    if (chunk.getType() == WebPChunkType.ANMF.value) {
274                        throw new ImagingException("Non animated image should not contain ANMF chunks");
275                    }
276
277                    if (chunk.getType() == WebPChunkType.VP8.value) {
278                        formatDetails = "WebP/Lossy (Extended)";
279                        colorType = ImageInfo.ColorType.YCbCr;
280                    } else if (chunk.getType() == WebPChunkType.VP8L.value) {
281                        formatDetails = "WebP/Lossless (Extended)";
282                    } else {
283                        throw new ImagingException("Unknown WebP chunk type: " + chunk);
284                    }
285                }
286            } else {
287                throw new ImagingException("Unknown WebP chunk type: " + chunk);
288            }
289
290            return new ImageInfo(formatDetails, 32, new ArrayList<>(), ImageFormats.WEBP, "webp", height, "image/webp", numberOfImages, -1, -1, -1, -1, width,
291                    false, hasAlpha, false, colorType, ImageInfo.CompressionAlgorithm.UNKNOWN);
292        }
293    }
294
295    @Override
296    public Dimension getImageSize(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException {
297        try (ChunksReader reader = new ChunksReader(byteSource)) {
298            final AbstractWebPChunk chunk = reader.readChunk();
299            if (chunk instanceof WebPChunkVp8) {
300                final WebPChunkVp8 vp8 = (WebPChunkVp8) chunk;
301                return new Dimension(vp8.getWidth(), vp8.getHeight());
302            }
303            if (chunk instanceof WebPChunkVp8l) {
304                final WebPChunkVp8l vp8l = (WebPChunkVp8l) chunk;
305                return new Dimension(vp8l.getImageWidth(), vp8l.getImageHeight());
306            }
307            if (chunk instanceof WebPChunkVp8x) {
308                final WebPChunkVp8x vp8x = (WebPChunkVp8x) chunk;
309                return new Dimension(vp8x.getCanvasWidth(), vp8x.getCanvasHeight());
310            }
311            throw new ImagingException("Unknown WebP chunk type: " + chunk);
312        }
313    }
314
315    @Override
316    public WebPImageMetadata getMetadata(final ByteSource byteSource, final WebPImagingParameters params) throws ImagingException, IOException {
317        try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.EXIF)) {
318            final AbstractWebPChunk chunk = reader.readChunk();
319            return chunk == null ? null : new WebPImageMetadata((TiffImageMetadata) new TiffImageParser().getMetadata(chunk.getBytes()));
320        }
321    }
322
323    @Override
324    public String getName() {
325        return "WebP-Custom";
326    }
327
328    @Override
329    public String getXmpXml(final ByteSource byteSource, final XmpImagingParameters<WebPImagingParameters> params) throws ImagingException, IOException {
330        try (ChunksReader reader = new ChunksReader(byteSource, WebPChunkType.XMP)) {
331            final WebPChunkXml chunk = (WebPChunkXml) reader.readChunk();
332            return chunk == null ? null : chunk.getXml();
333        }
334    }
335}