1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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
51
52
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);
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;
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
138
139
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
161
162 public WebPImageParser() {
163
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
177
178
179
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 }