View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one or more
3    * contributor license agreements.  See the NOTICE file distributed with
4    * this work for additional information regarding copyright ownership.
5    * The ASF licenses this file to You under the Apache License, Version 2.0
6    * (the "License"); you may not use this file except in compliance with
7    * the License.  You may obtain a copy of the License at
8    *
9    *      http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.apache.commons.imaging.formats.webp;
18  
19  import static org.apache.commons.imaging.common.BinaryFunctions.read4Bytes;
20  import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
21  import static org.apache.commons.imaging.common.BinaryFunctions.skipBytes;
22  
23  import java.awt.Dimension;
24  import java.awt.image.BufferedImage;
25  import java.io.Closeable;
26  import java.io.IOException;
27  import java.io.InputStream;
28  import java.io.PrintWriter;
29  import java.nio.ByteOrder;
30  import java.util.ArrayList;
31  
32  import org.apache.commons.imaging.AbstractImageParser;
33  import org.apache.commons.imaging.ImageFormat;
34  import org.apache.commons.imaging.ImageFormats;
35  import org.apache.commons.imaging.ImageInfo;
36  import org.apache.commons.imaging.ImagingException;
37  import org.apache.commons.imaging.bytesource.ByteSource;
38  import org.apache.commons.imaging.common.XmpEmbeddable;
39  import org.apache.commons.imaging.common.XmpImagingParameters;
40  import org.apache.commons.imaging.formats.tiff.TiffImageMetadata;
41  import org.apache.commons.imaging.formats.tiff.TiffImageParser;
42  import org.apache.commons.imaging.formats.webp.chunks.AbstractWebPChunk;
43  import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8;
44  import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8l;
45  import org.apache.commons.imaging.formats.webp.chunks.WebPChunkVp8x;
46  import org.apache.commons.imaging.formats.webp.chunks.WebPChunkXml;
47  import org.apache.commons.imaging.internal.SafeOperations;
48  
49  /**
50   * WebP image parser.
51   *
52   * @since 1.0.0-alpha4
53   */
54  public class WebPImageParser extends AbstractImageParser<WebPImagingParameters> implements XmpEmbeddable<WebPImagingParameters> {
55  
56      private static final class ChunksReader implements Closeable {
57          private final InputStream is;
58          private final WebPChunkType[] chunkTypes;
59          private int sizeCount = 4;
60          private boolean firstChunk = true;
61  
62          final int fileSize;
63  
64          ChunksReader(final ByteSource byteSource) throws IOException, ImagingException {
65              this(byteSource, (WebPChunkType[]) null);
66          }
67  
68          ChunksReader(final ByteSource byteSource, final WebPChunkType... chunkTypes) throws ImagingException, IOException {
69              this.is = byteSource.getInputStream();
70              this.chunkTypes = chunkTypes;
71              this.fileSize = readFileHeader(is);
72          }
73  
74          @Override
75          public void close() throws IOException {
76              is.close();
77          }
78  
79          int getOffset() {
80              return SafeOperations.add(sizeCount, 8); // File Header
81          }
82  
83          AbstractWebPChunk readChunk() throws ImagingException, IOException {
84              while (sizeCount < fileSize) {
85                  final int type = read4Bytes("Chunk Type", is, "Not a valid WebP file", ByteOrder.LITTLE_ENDIAN);
86                  final int payloadSize = read4Bytes("Chunk Size", is, "Not a valid WebP file", ByteOrder.LITTLE_ENDIAN);
87                  if (payloadSize < 0) {
88                      throw new ImagingException("Chunk Payload is too long:" + payloadSize);
89                  }
90                  final boolean padding = payloadSize % 2 != 0;
91                  final int chunkSize = SafeOperations.add(8, padding ? 1 : 0, payloadSize);
92  
93                  if (firstChunk) {
94                      firstChunk = false;
95                      if (type != WebPChunkType.VP8.value && type != WebPChunkType.VP8L.value && type != WebPChunkType.VP8X.value) {
96                          throw new ImagingException("First Chunk must be VP8, VP8L or VP8X");
97                      }
98                  }
99  
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 }