1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 package org.apache.commons.imaging.formats.gif;
18
19 import static org.apache.commons.imaging.common.BinaryFunctions.compareBytes;
20 import static org.apache.commons.imaging.common.BinaryFunctions.logByteBits;
21 import static org.apache.commons.imaging.common.BinaryFunctions.logCharQuad;
22 import static org.apache.commons.imaging.common.BinaryFunctions.read2Bytes;
23 import static org.apache.commons.imaging.common.BinaryFunctions.readByte;
24 import static org.apache.commons.imaging.common.BinaryFunctions.readBytes;
25
26 import java.awt.Dimension;
27 import java.awt.image.BufferedImage;
28 import java.io.ByteArrayInputStream;
29 import java.io.IOException;
30 import java.io.InputStream;
31 import java.io.OutputStream;
32 import java.io.PrintWriter;
33 import java.nio.ByteOrder;
34 import java.nio.charset.StandardCharsets;
35 import java.util.ArrayList;
36 import java.util.List;
37 import java.util.logging.Level;
38 import java.util.logging.Logger;
39
40 import org.apache.commons.imaging.AbstractImageParser;
41 import org.apache.commons.imaging.FormatCompliance;
42 import org.apache.commons.imaging.ImageFormat;
43 import org.apache.commons.imaging.ImageFormats;
44 import org.apache.commons.imaging.ImageInfo;
45 import org.apache.commons.imaging.ImagingException;
46 import org.apache.commons.imaging.bytesource.ByteSource;
47 import org.apache.commons.imaging.common.Allocator;
48 import org.apache.commons.imaging.common.BinaryOutputStream;
49 import org.apache.commons.imaging.common.ImageBuilder;
50 import org.apache.commons.imaging.common.ImageMetadata;
51 import org.apache.commons.imaging.common.XmpEmbeddable;
52 import org.apache.commons.imaging.common.XmpImagingParameters;
53 import org.apache.commons.imaging.mylzw.MyLzwCompressor;
54 import org.apache.commons.imaging.mylzw.MyLzwDecompressor;
55 import org.apache.commons.imaging.palette.Palette;
56 import org.apache.commons.imaging.palette.PaletteFactory;
57
58 public class GifImageParser extends AbstractImageParser<GifImagingParameters> implements XmpEmbeddable<GifImagingParameters> {
59
60 private static final Logger LOGGER = Logger.getLogger(GifImageParser.class.getName());
61
62 private static final String DEFAULT_EXTENSION = ImageFormats.GIF.getDefaultExtension();
63 private static final String[] ACCEPTED_EXTENSIONS = ImageFormats.GIF.getExtensions();
64 private static final byte[] GIF_HEADER_SIGNATURE = { 71, 73, 70 };
65 private static final int EXTENSION_CODE = 0x21;
66 private static final int IMAGE_SEPARATOR = 0x2C;
67 private static final int GRAPHIC_CONTROL_EXTENSION = EXTENSION_CODE << 8 | 0xf9;
68 private static final int COMMENT_EXTENSION = 0xfe;
69 private static final int PLAIN_TEXT_EXTENSION = 0x01;
70 private static final int XMP_EXTENSION = 0xff;
71 private static final int TERMINATOR_BYTE = 0x3b;
72 private static final int APPLICATION_EXTENSION_LABEL = 0xff;
73 private static final int XMP_COMPLETE_CODE = EXTENSION_CODE << 8 | XMP_EXTENSION;
74 private static final int LOCAL_COLOR_TABLE_FLAG_MASK = 1 << 7;
75 private static final int INTERLACE_FLAG_MASK = 1 << 6;
76 private static final int SORT_FLAG_MASK = 1 << 5;
77 private static final byte[] XMP_APPLICATION_ID_AND_AUTH_CODE = { 0x58,
78 0x4D,
79 0x50,
80 0x20,
81 0x44,
82 0x61,
83 0x74,
84 0x61,
85 0x58,
86 0x4D,
87 0x50,
88 };
89
90
91 static DisposalMethod createDisposalMethodFromIntValue(final int value) throws ImagingException {
92 switch (value) {
93 case 0:
94 return DisposalMethod.UNSPECIFIED;
95 case 1:
96 return DisposalMethod.DO_NOT_DISPOSE;
97 case 2:
98 return DisposalMethod.RESTORE_TO_BACKGROUND;
99 case 3:
100 return DisposalMethod.RESTORE_TO_PREVIOUS;
101 case 4:
102 return DisposalMethod.TO_BE_DEFINED_1;
103 case 5:
104 return DisposalMethod.TO_BE_DEFINED_2;
105 case 6:
106 return DisposalMethod.TO_BE_DEFINED_3;
107 case 7:
108 return DisposalMethod.TO_BE_DEFINED_4;
109 default:
110 throw new ImagingException("GIF: Invalid parsing of disposal method");
111 }
112 }
113
114 public GifImageParser() {
115 super(ByteOrder.LITTLE_ENDIAN);
116 }
117
118 private int convertColorTableSize(final int tableSize) {
119 return 3 * simplePow(2, tableSize + 1);
120 }
121
122 @Override
123 public boolean dumpImageFile(final PrintWriter pw, final ByteSource byteSource) throws ImagingException, IOException {
124 pw.println("gif.dumpImageFile");
125
126 final ImageInfo imageData = getImageInfo(byteSource);
127 if (imageData == null) {
128 return false;
129 }
130
131 imageData.toString(pw, "");
132
133 final GifImageContents blocks = readFile(byteSource, false);
134
135 pw.println("gif.blocks: " + blocks.blocks.size());
136 for (int i = 0; i < blocks.blocks.size(); i++) {
137 final GifBlock gifBlock = blocks.blocks.get(i);
138 this.debugNumber(pw, "\t" + i + " (" + gifBlock.getClass().getName() + ")", gifBlock.blockCode, 4);
139 }
140
141 pw.println("");
142
143 return true;
144 }
145
146
147
148
149
150 @SuppressWarnings("unchecked")
151 private <T extends GifBlock> List<T> findAllBlocks(final List<GifBlock> blocks, final int code) {
152 final List<T> filteredBlocks = new ArrayList<>();
153 for (final GifBlock gifBlock : blocks) {
154 if (gifBlock.blockCode == code) {
155 filteredBlocks.add((T) gifBlock);
156 }
157 }
158 return filteredBlocks;
159 }
160
161 private List<GifImageData> findAllImageData(final GifImageContents imageContents) throws ImagingException {
162 final List<ImageDescriptor> descriptors = findAllBlocks(imageContents.blocks, IMAGE_SEPARATOR);
163
164 if (descriptors.isEmpty()) {
165 throw new ImagingException("GIF: Couldn't read Image Descriptor");
166 }
167
168 final List<GraphicControlExtension> gcExtensions = findAllBlocks(imageContents.blocks, GRAPHIC_CONTROL_EXTENSION);
169
170 if (!gcExtensions.isEmpty() && gcExtensions.size() != descriptors.size()) {
171 throw new ImagingException("GIF: Invalid amount of Graphic Control Extensions");
172 }
173
174 final List<GifImageData> imageData = Allocator.arrayList(descriptors.size());
175 for (int i = 0; i < descriptors.size(); i++) {
176 final ImageDescriptor descriptor = descriptors.get(i);
177 if (descriptor == null) {
178 throw new ImagingException(String.format("GIF: Couldn't read Image Descriptor of image number %d", i));
179 }
180
181 final GraphicControlExtension gce = gcExtensions.isEmpty() ? null : gcExtensions.get(i);
182
183 imageData.add(new GifImageData(descriptor, gce));
184 }
185
186 return imageData;
187 }
188
189 private GifBlock findBlock(final List<GifBlock> blocks, final int code) {
190 for (final GifBlock gifBlock : blocks) {
191 if (gifBlock.blockCode == code) {
192 return gifBlock;
193 }
194 }
195 return null;
196 }
197
198 private GifImageData findFirstImageData(final GifImageContents imageContents) throws ImagingException {
199 final ImageDescriptor descriptor = (ImageDescriptor) findBlock(imageContents.blocks, IMAGE_SEPARATOR);
200
201 if (descriptor == null) {
202 throw new ImagingException("GIF: Couldn't read Image Descriptor");
203 }
204
205 final GraphicControlExtension gce = (GraphicControlExtension) findBlock(imageContents.blocks, GRAPHIC_CONTROL_EXTENSION);
206
207 return new GifImageData(descriptor, gce);
208 }
209
210 @Override
211 protected String[] getAcceptedExtensions() {
212 return ACCEPTED_EXTENSIONS;
213 }
214
215 @Override
216 protected ImageFormat[] getAcceptedTypes() {
217 return new ImageFormat[] { ImageFormats.GIF,
218 };
219 }
220
221 @Override
222 public List<BufferedImage> getAllBufferedImages(final ByteSource byteSource) throws ImagingException, IOException {
223 final GifImageContents imageContents = readFile(byteSource, false);
224
225 final GifHeaderInfo ghi = imageContents.gifHeaderInfo;
226 if (ghi == null) {
227 throw new ImagingException("GIF: Couldn't read Header");
228 }
229
230 final List<GifImageData> imageData = findAllImageData(imageContents);
231 final List<BufferedImage> result = Allocator.arrayList(imageData.size());
232 for (final GifImageData id : imageData) {
233 result.add(getBufferedImage(ghi, id, imageContents.globalColorTable));
234 }
235 return result;
236 }
237
238 @Override
239 public BufferedImage getBufferedImage(final ByteSource byteSource, final GifImagingParameters params) throws ImagingException, IOException {
240 final GifImageContents imageContents = readFile(byteSource, false);
241
242 final GifHeaderInfo ghi = imageContents.gifHeaderInfo;
243 if (ghi == null) {
244 throw new ImagingException("GIF: Couldn't read Header");
245 }
246
247 final GifImageData imageData = findFirstImageData(imageContents);
248
249 return getBufferedImage(ghi, imageData, imageContents.globalColorTable);
250 }
251
252 private BufferedImage getBufferedImage(final GifHeaderInfo headerInfo, final GifImageData imageData, final byte[] globalColorTable)
253 throws ImagingException {
254 final ImageDescriptor id = imageData.descriptor;
255 final GraphicControlExtension gce = imageData.gce;
256
257 final int width = id.imageWidth;
258 final int height = id.imageHeight;
259
260 boolean hasAlpha = false;
261 if (gce != null && gce.transparency) {
262 hasAlpha = true;
263 }
264
265 final ImageBuilder imageBuilder = new ImageBuilder(width, height, hasAlpha);
266
267 int[] colorTable;
268 if (id.localColorTable != null) {
269 colorTable = getColorTable(id.localColorTable);
270 } else if (globalColorTable != null) {
271 colorTable = getColorTable(globalColorTable);
272 } else {
273 throw new ImagingException("Gif: No Color Table");
274 }
275
276 int transparentIndex = -1;
277 if (gce != null && hasAlpha) {
278 transparentIndex = gce.transparentColorIndex;
279 }
280
281 int counter = 0;
282
283 final int rowsInPass1 = (height + 7) / 8;
284 final int rowsInPass2 = (height + 3) / 8;
285 final int rowsInPass3 = (height + 1) / 4;
286 final int rowsInPass4 = height / 2;
287
288 for (int row = 0; row < height; row++) {
289 int y;
290 if (id.interlaceFlag) {
291 int theRow = row;
292 if (theRow < rowsInPass1) {
293 y = theRow * 8;
294 } else {
295 theRow -= rowsInPass1;
296 if (theRow < rowsInPass2) {
297 y = 4 + theRow * 8;
298 } else {
299 theRow -= rowsInPass2;
300 if (theRow < rowsInPass3) {
301 y = 2 + theRow * 4;
302 } else {
303 theRow -= rowsInPass3;
304 if (theRow >= rowsInPass4) {
305 throw new ImagingException("Gif: Strange Row");
306 }
307 y = 1 + theRow * 2;
308 }
309 }
310 }
311 } else {
312 y = row;
313 }
314
315 for (int x = 0; x < width; x++) {
316 if (counter >= id.imageData.length) {
317 throw new ImagingException(
318 String.format("Invalid GIF image data length [%d], greater than the image data length [%d]", id.imageData.length, width));
319 }
320 final int index = 0xff & id.imageData[counter++];
321 if (index >= colorTable.length) {
322 throw new ImagingException(
323 String.format("Invalid GIF color table index [%d], greater than the color table length [%d]", index, colorTable.length));
324 }
325 int rgb = colorTable[index];
326
327 if (transparentIndex == index) {
328 rgb = 0x00;
329 }
330 imageBuilder.setRgb(x, y, rgb);
331 }
332 }
333
334 return imageBuilder.getBufferedImage();
335 }
336
337 private int[] getColorTable(final byte[] bytes) throws ImagingException {
338 if (bytes.length % 3 != 0) {
339 throw new ImagingException("Bad Color Table Length: " + bytes.length);
340 }
341 final int length = bytes.length / 3;
342
343 final int[] result = Allocator.intArray(length);
344
345 for (int i = 0; i < length; i++) {
346 final int red = 0xff & bytes[i * 3 + 0];
347 final int green = 0xff & bytes[i * 3 + 1];
348 final int blue = 0xff & bytes[i * 3 + 2];
349
350 final int alpha = 0xff;
351
352 final int rgb = alpha << 24 | red << 16 | green << 8 | blue << 0;
353 result[i] = rgb;
354 }
355
356 return result;
357 }
358
359 private List<String> getComments(final List<GifBlock> blocks) throws IOException {
360 final List<String> result = new ArrayList<>();
361 final int code = 0x21fe;
362
363 for (final GifBlock block : blocks) {
364 if (block.blockCode == code) {
365 final byte[] bytes = ((GenericGifBlock) block).appendSubBlocks();
366 result.add(new String(bytes, StandardCharsets.US_ASCII));
367 }
368 }
369
370 return result;
371 }
372
373 @Override
374 public String getDefaultExtension() {
375 return DEFAULT_EXTENSION;
376 }
377
378 @Override
379 public GifImagingParameters getDefaultParameters() {
380 return new GifImagingParameters();
381 }
382
383 @Override
384 public FormatCompliance getFormatCompliance(final ByteSource byteSource) throws ImagingException, IOException {
385 final FormatCompliance result = new FormatCompliance(byteSource.toString());
386
387 readFile(byteSource, false, result);
388
389 return result;
390 }
391
392 @Override
393 public byte[] getIccProfileBytes(final ByteSource byteSource, final GifImagingParameters params) throws ImagingException, IOException {
394 return null;
395 }
396
397 @Override
398 public ImageInfo getImageInfo(final ByteSource byteSource, final GifImagingParameters params) throws ImagingException, IOException {
399 final GifImageContents blocks = readFile(byteSource, GifImagingParameters.getStopReadingBeforeImageData(params));
400
401 final GifHeaderInfo bhi = blocks.gifHeaderInfo;
402 if (bhi == null) {
403 throw new ImagingException("GIF: Couldn't read Header");
404 }
405
406 final ImageDescriptor id = (ImageDescriptor) findBlock(blocks.blocks, IMAGE_SEPARATOR);
407 if (id == null) {
408 throw new ImagingException("GIF: Couldn't read ImageDescriptor");
409 }
410
411 final GraphicControlExtension gce = (GraphicControlExtension) findBlock(blocks.blocks, GRAPHIC_CONTROL_EXTENSION);
412
413 final int height = bhi.logicalScreenHeight;
414 final int width = bhi.logicalScreenWidth;
415
416 final List<String> comments = getComments(blocks.blocks);
417 final int bitsPerPixel = bhi.colorResolution + 1;
418 final ImageFormat format = ImageFormats.GIF;
419 final String formatName = "Graphics Interchange Format";
420 final String mimeType = "image/gif";
421
422 final int numberOfImages = findAllBlocks(blocks.blocks, IMAGE_SEPARATOR).size();
423
424 final boolean progressive = id.interlaceFlag;
425
426 final int physicalWidthDpi = 72;
427 final float physicalWidthInch = (float) ((double) width / (double) physicalWidthDpi);
428 final int physicalHeightDpi = 72;
429 final float physicalHeightInch = (float) ((double) height / (double) physicalHeightDpi);
430
431 final String formatDetails = "GIF " + (char) blocks.gifHeaderInfo.version1 + (char) blocks.gifHeaderInfo.version2
432 + (char) blocks.gifHeaderInfo.version3;
433
434 boolean transparent = false;
435 if (gce != null && gce.transparency) {
436 transparent = true;
437 }
438
439 final boolean usesPalette = true;
440 final ImageInfo.ColorType colorType = ImageInfo.ColorType.RGB;
441 final ImageInfo.CompressionAlgorithm compressionAlgorithm = ImageInfo.CompressionAlgorithm.LZW;
442
443 return new ImageInfo(formatDetails, bitsPerPixel, comments, format, formatName, height, mimeType, numberOfImages, physicalHeightDpi, physicalHeightInch,
444 physicalWidthDpi, physicalWidthInch, width, progressive, transparent, usesPalette, colorType, compressionAlgorithm);
445 }
446
447 @Override
448 public Dimension getImageSize(final ByteSource byteSource, final GifImagingParameters params) throws ImagingException, IOException {
449 final GifImageContents blocks = readFile(byteSource, false);
450
451 final GifHeaderInfo bhi = blocks.gifHeaderInfo;
452 if (bhi == null) {
453 throw new ImagingException("GIF: Couldn't read Header");
454 }
455
456
457
458
459
460
461 return new Dimension(bhi.logicalScreenWidth, bhi.logicalScreenHeight);
462 }
463
464 @Override
465 public ImageMetadata getMetadata(final ByteSource byteSource, final GifImagingParameters params) throws ImagingException, IOException {
466 final GifImageContents imageContents = readFile(byteSource, GifImagingParameters.getStopReadingBeforeImageData(params));
467
468 final GifHeaderInfo bhi = imageContents.gifHeaderInfo;
469 if (bhi == null) {
470 throw new ImagingException("GIF: Couldn't read Header");
471 }
472
473 final List<GifImageData> imageData = findAllImageData(imageContents);
474 final List<GifImageMetadataItem> metadataItems = Allocator.arrayList(imageData.size());
475 for (final GifImageData id : imageData) {
476 final DisposalMethod disposalMethod = createDisposalMethodFromIntValue(id.gce.dispose);
477 metadataItems.add(new GifImageMetadataItem(id.gce.delay, id.descriptor.imageLeftPosition, id.descriptor.imageTopPosition, disposalMethod));
478 }
479 return new GifImageMetadata(bhi.logicalScreenWidth, bhi.logicalScreenHeight, metadataItems);
480 }
481
482 @Override
483 public String getName() {
484 return "Graphics Interchange Format";
485 }
486
487
488
489
490
491
492
493
494
495 @Override
496 public String getXmpXml(final ByteSource byteSource, final XmpImagingParameters<GifImagingParameters> params) throws ImagingException, IOException {
497 try (InputStream is = byteSource.getInputStream()) {
498 final GifHeaderInfo ghi = readHeader(is, null);
499
500 if (ghi.globalColorTableFlag) {
501 readColorTable(is, ghi.sizeOfGlobalColorTable);
502 }
503
504 final List<GifBlock> blocks = readBlocks(ghi, is, true, null);
505
506 final List<String> result = new ArrayList<>();
507 for (final GifBlock block : blocks) {
508 if (block.blockCode != XMP_COMPLETE_CODE) {
509 continue;
510 }
511
512 final GenericGifBlock genericBlock = (GenericGifBlock) block;
513
514 final byte[] blockBytes = genericBlock.appendSubBlocks(true);
515 if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length) {
516 continue;
517 }
518
519 if (!compareBytes(blockBytes, 0, XMP_APPLICATION_ID_AND_AUTH_CODE, 0, XMP_APPLICATION_ID_AND_AUTH_CODE.length)) {
520 continue;
521 }
522
523 final byte[] gifMagicTrailer = new byte[256];
524 for (int magic = 0; magic <= 0xff; magic++) {
525 gifMagicTrailer[magic] = (byte) (0xff - magic);
526 }
527
528 if (blockBytes.length < XMP_APPLICATION_ID_AND_AUTH_CODE.length + gifMagicTrailer.length) {
529 continue;
530 }
531 if (!compareBytes(blockBytes, blockBytes.length - gifMagicTrailer.length, gifMagicTrailer, 0, gifMagicTrailer.length)) {
532 throw new ImagingException("XMP block in GIF missing magic trailer.");
533 }
534
535
536 final String xml = new String(blockBytes, XMP_APPLICATION_ID_AND_AUTH_CODE.length,
537 blockBytes.length - (XMP_APPLICATION_ID_AND_AUTH_CODE.length + gifMagicTrailer.length), StandardCharsets.UTF_8);
538 result.add(xml);
539 }
540
541 if (result.isEmpty()) {
542 return null;
543 }
544 if (result.size() > 1) {
545 throw new ImagingException("More than one XMP Block in GIF.");
546 }
547 return result.get(0);
548 }
549 }
550
551 private List<GifBlock> readBlocks(final GifHeaderInfo ghi, final InputStream is, final boolean stopBeforeImageData, final FormatCompliance formatCompliance)
552 throws ImagingException, IOException {
553 final List<GifBlock> result = new ArrayList<>();
554
555 while (true) {
556 final int code = is.read();
557
558 switch (code) {
559 case -1:
560 throw new ImagingException("GIF: unexpected end of data");
561
562 case IMAGE_SEPARATOR:
563 final ImageDescriptor id = readImageDescriptor(ghi, code, is, stopBeforeImageData, formatCompliance);
564 result.add(id);
565
566
567
568 break;
569
570 case EXTENSION_CODE: {
571 final int extensionCode = is.read();
572 final int completeCode = (0xff & code) << 8 | 0xff & extensionCode;
573
574 switch (extensionCode) {
575 case 0xf9:
576 final GraphicControlExtension gce = readGraphicControlExtension(completeCode, is);
577 result.add(gce);
578 break;
579
580 case COMMENT_EXTENSION:
581 case PLAIN_TEXT_EXTENSION: {
582 final GenericGifBlock block = readGenericGifBlock(is, completeCode);
583 result.add(block);
584 break;
585 }
586
587 case APPLICATION_EXTENSION_LABEL: {
588
589
590 final byte[] label = readSubBlock(is);
591
592 if (formatCompliance != null) {
593 formatCompliance.addComment("Unknown Application Extension (" + new String(label, StandardCharsets.US_ASCII) + ")", completeCode);
594 }
595
596 if (label.length > 0) {
597 final GenericGifBlock block = readGenericGifBlock(is, completeCode, label);
598 result.add(block);
599 }
600 break;
601 }
602
603 default: {
604
605 if (formatCompliance != null) {
606 formatCompliance.addComment("Unknown block", completeCode);
607 }
608
609 final GenericGifBlock block = readGenericGifBlock(is, completeCode);
610 result.add(block);
611 break;
612 }
613 }
614 }
615 break;
616
617 case TERMINATOR_BYTE:
618 return result;
619
620 case 0x00:
621 break;
622
623 default:
624 throw new ImagingException("GIF: unknown code: " + code);
625 }
626 }
627 }
628
629 private byte[] readColorTable(final InputStream is, final int tableSize) throws IOException {
630 final int actualSize = convertColorTableSize(tableSize);
631
632 return readBytes("block", is, actualSize, "GIF: corrupt Color Table");
633 }
634
635 private GifImageContents readFile(final ByteSource byteSource, final boolean stopBeforeImageData) throws ImagingException, IOException {
636 return readFile(byteSource, stopBeforeImageData, FormatCompliance.getDefault());
637 }
638
639 private GifImageContents readFile(final ByteSource byteSource, final boolean stopBeforeImageData, final FormatCompliance formatCompliance)
640 throws ImagingException, IOException {
641 try (InputStream is = byteSource.getInputStream()) {
642 final GifHeaderInfo ghi = readHeader(is, formatCompliance);
643
644 byte[] globalColorTable = null;
645 if (ghi.globalColorTableFlag) {
646 globalColorTable = readColorTable(is, ghi.sizeOfGlobalColorTable);
647 }
648
649 final List<GifBlock> blocks = readBlocks(ghi, is, stopBeforeImageData, formatCompliance);
650
651 return new GifImageContents(ghi, globalColorTable, blocks);
652 }
653 }
654
655 private GenericGifBlock readGenericGifBlock(final InputStream is, final int code) throws IOException {
656 return readGenericGifBlock(is, code, null);
657 }
658
659 private GenericGifBlock readGenericGifBlock(final InputStream is, final int code, final byte[] first) throws IOException {
660 final List<byte[]> subBlocks = new ArrayList<>();
661
662 if (first != null) {
663 subBlocks.add(first);
664 }
665
666 while (true) {
667 final byte[] bytes = readSubBlock(is);
668 if (bytes.length < 1) {
669 break;
670 }
671 subBlocks.add(bytes);
672 }
673
674 return new GenericGifBlock(code, subBlocks);
675 }
676
677 private GraphicControlExtension readGraphicControlExtension(final int code, final InputStream is) throws IOException {
678 readByte("block_size", is, "GIF: corrupt GraphicControlExt");
679 final int packed = readByte("packed fields", is, "GIF: corrupt GraphicControlExt");
680
681 final int dispose = (packed & 0x1c) >> 2;
682 final boolean transparency = (packed & 1) != 0;
683
684 final int delay = read2Bytes("delay in milliseconds", is, "GIF: corrupt GraphicControlExt", getByteOrder());
685 final int transparentColorIndex = 0xff & readByte("transparent color index", is, "GIF: corrupt GraphicControlExt");
686 readByte("block terminator", is, "GIF: corrupt GraphicControlExt");
687
688 return new GraphicControlExtension(code, packed, dispose, transparency, delay, transparentColorIndex);
689 }
690
691 private GifHeaderInfo readHeader(final InputStream is, final FormatCompliance formatCompliance) throws ImagingException, IOException {
692 final byte identifier1 = readByte("identifier1", is, "Not a Valid GIF File");
693 final byte identifier2 = readByte("identifier2", is, "Not a Valid GIF File");
694 final byte identifier3 = readByte("identifier3", is, "Not a Valid GIF File");
695
696 final byte version1 = readByte("version1", is, "Not a Valid GIF File");
697 final byte version2 = readByte("version2", is, "Not a Valid GIF File");
698 final byte version3 = readByte("version3", is, "Not a Valid GIF File");
699
700 if (formatCompliance != null) {
701 formatCompliance.compareBytes("Signature", GIF_HEADER_SIGNATURE, new byte[] { identifier1, identifier2, identifier3 });
702 formatCompliance.compare("version", 56, version1);
703 formatCompliance.compare("version", new int[] { 55, 57, }, version2);
704 formatCompliance.compare("version", 97, version3);
705 }
706
707 if (LOGGER.isLoggable(Level.FINEST)) {
708 logCharQuad("identifier: ", identifier1 << 16 | identifier2 << 8 | identifier3 << 0);
709 logCharQuad("version: ", version1 << 16 | version2 << 8 | version3 << 0);
710 }
711
712 final int logicalScreenWidth = read2Bytes("Logical Screen Width", is, "Not a Valid GIF File", getByteOrder());
713 final int logicalScreenHeight = read2Bytes("Logical Screen Height", is, "Not a Valid GIF File", getByteOrder());
714
715 if (formatCompliance != null) {
716 formatCompliance.checkBounds("Width", 1, Integer.MAX_VALUE, logicalScreenWidth);
717 formatCompliance.checkBounds("Height", 1, Integer.MAX_VALUE, logicalScreenHeight);
718 }
719
720 final byte packedFields = readByte("Packed Fields", is, "Not a Valid GIF File");
721 final byte backgroundColorIndex = readByte("Background Color Index", is, "Not a Valid GIF File");
722 final byte pixelAspectRatio = readByte("Pixel Aspect Ratio", is, "Not a Valid GIF File");
723
724 if (LOGGER.isLoggable(Level.FINEST)) {
725 logByteBits("PackedFields bits", packedFields);
726 }
727
728 final boolean globalColorTableFlag = (packedFields & 128) > 0;
729 if (LOGGER.isLoggable(Level.FINEST)) {
730 LOGGER.finest("GlobalColorTableFlag: " + globalColorTableFlag);
731 }
732 final byte colorResolution = (byte) (packedFields >> 4 & 7);
733 if (LOGGER.isLoggable(Level.FINEST)) {
734 LOGGER.finest("ColorResolution: " + colorResolution);
735 }
736 final boolean sortFlag = (packedFields & 8) > 0;
737 if (LOGGER.isLoggable(Level.FINEST)) {
738 LOGGER.finest("SortFlag: " + sortFlag);
739 }
740 final byte sizeofGlobalColorTable = (byte) (packedFields & 7);
741 if (LOGGER.isLoggable(Level.FINEST)) {
742 LOGGER.finest("SizeofGlobalColorTable: " + sizeofGlobalColorTable);
743 }
744
745 if (formatCompliance != null) {
746 if (globalColorTableFlag && backgroundColorIndex != -1) {
747 formatCompliance.checkBounds("Background Color Index", 0, convertColorTableSize(sizeofGlobalColorTable), backgroundColorIndex);
748 }
749 }
750
751 return new GifHeaderInfo(identifier1, identifier2, identifier3, version1, version2, version3, logicalScreenWidth, logicalScreenHeight, packedFields,
752 backgroundColorIndex, pixelAspectRatio, globalColorTableFlag, colorResolution, sortFlag, sizeofGlobalColorTable);
753 }
754
755 private ImageDescriptor readImageDescriptor(final GifHeaderInfo ghi, final int blockCode, final InputStream is, final boolean stopBeforeImageData,
756 final FormatCompliance formatCompliance) throws ImagingException, IOException {
757 final int imageLeftPosition = read2Bytes("Image Left Position", is, "Not a Valid GIF File", getByteOrder());
758 final int imageTopPosition = read2Bytes("Image Top Position", is, "Not a Valid GIF File", getByteOrder());
759 final int imageWidth = read2Bytes("Image Width", is, "Not a Valid GIF File", getByteOrder());
760 final int imageHeight = read2Bytes("Image Height", is, "Not a Valid GIF File", getByteOrder());
761 final byte packedFields = readByte("Packed Fields", is, "Not a Valid GIF File");
762
763 if (formatCompliance != null) {
764 formatCompliance.checkBounds("Width", 1, ghi.logicalScreenWidth, imageWidth);
765 formatCompliance.checkBounds("Height", 1, ghi.logicalScreenHeight, imageHeight);
766 formatCompliance.checkBounds("Left Position", 0, ghi.logicalScreenWidth - imageWidth, imageLeftPosition);
767 formatCompliance.checkBounds("Top Position", 0, ghi.logicalScreenHeight - imageHeight, imageTopPosition);
768 }
769
770 if (LOGGER.isLoggable(Level.FINEST)) {
771 logByteBits("PackedFields bits", packedFields);
772 }
773
774 final boolean localColorTableFlag = (packedFields >> 7 & 1) > 0;
775 if (LOGGER.isLoggable(Level.FINEST)) {
776 LOGGER.finest("LocalColorTableFlag: " + localColorTableFlag);
777 }
778 final boolean interlaceFlag = (packedFields >> 6 & 1) > 0;
779 if (LOGGER.isLoggable(Level.FINEST)) {
780 LOGGER.finest("Interlace Flag: " + interlaceFlag);
781 }
782 final boolean sortFlag = (packedFields >> 5 & 1) > 0;
783 if (LOGGER.isLoggable(Level.FINEST)) {
784 LOGGER.finest("Sort Flag: " + sortFlag);
785 }
786
787 final byte sizeOfLocalColorTable = (byte) (packedFields & 7);
788 if (LOGGER.isLoggable(Level.FINEST)) {
789 LOGGER.finest("SizeofLocalColorTable: " + sizeOfLocalColorTable);
790 }
791
792 byte[] localColorTable = null;
793 if (localColorTableFlag) {
794 localColorTable = readColorTable(is, sizeOfLocalColorTable);
795 }
796
797 byte[] imageData = null;
798 if (!stopBeforeImageData) {
799 final int lzwMinimumCodeSize = is.read();
800
801 final GenericGifBlock block = readGenericGifBlock(is, -1);
802 final byte[] bytes = block.appendSubBlocks();
803 final InputStream bais = new ByteArrayInputStream(bytes);
804
805 final int size = imageWidth * imageHeight;
806 final MyLzwDecompressor myLzwDecompressor = new MyLzwDecompressor(lzwMinimumCodeSize, ByteOrder.LITTLE_ENDIAN, false);
807 imageData = myLzwDecompressor.decompress(bais, size);
808 } else {
809 final int LZWMinimumCodeSize = is.read();
810 if (LOGGER.isLoggable(Level.FINEST)) {
811 LOGGER.finest("LZWMinimumCodeSize: " + LZWMinimumCodeSize);
812 }
813
814 readGenericGifBlock(is, -1);
815 }
816
817 return new ImageDescriptor(blockCode, imageLeftPosition, imageTopPosition, imageWidth, imageHeight, packedFields, localColorTableFlag, interlaceFlag,
818 sortFlag, sizeOfLocalColorTable, localColorTable, imageData);
819 }
820
821 private byte[] readSubBlock(final InputStream is) throws IOException {
822 final int blockSize = 0xff & readByte("blockSize", is, "GIF: corrupt block");
823
824 return readBytes("block", is, blockSize, "GIF: corrupt block");
825 }
826
827 private int simplePow(final int base, final int power) {
828 int result = 1;
829
830 for (int i = 0; i < power; i++) {
831 result *= base;
832 }
833
834 return result;
835 }
836
837 private void writeAsSubBlocks(final OutputStream os, final byte[] bytes) throws IOException {
838 int index = 0;
839
840 while (index < bytes.length) {
841 final int blockSize = Math.min(bytes.length - index, 255);
842 os.write(blockSize);
843 os.write(bytes, index, blockSize);
844 index += blockSize;
845 }
846 os.write(0);
847 }
848
849 @Override
850 public void writeImage(final BufferedImage src, final OutputStream os, GifImagingParameters params) throws ImagingException, IOException {
851 if (params == null) {
852 params = new GifImagingParameters();
853 }
854
855 final String xmpXml = params.getXmpXml();
856
857 final int width = src.getWidth();
858 final int height = src.getHeight();
859
860 final boolean hasAlpha = new PaletteFactory().hasTransparency(src);
861
862 final int maxColors = hasAlpha ? 255 : 256;
863
864 Palette palette2 = new PaletteFactory().makeExactRgbPaletteSimple(src, maxColors);
865
866
867
868 if (palette2 == null) {
869 palette2 = new PaletteFactory().makeQuantizedRgbPalette(src, maxColors);
870 if (LOGGER.isLoggable(Level.FINE)) {
871 LOGGER.fine("quantizing");
872 }
873 } else if (LOGGER.isLoggable(Level.FINE)) {
874 LOGGER.fine("exact palette");
875 }
876
877 if (palette2 == null) {
878 throw new ImagingException("Gif: can't write images with more than 256 colors");
879 }
880 final int paletteSize = palette2.length() + (hasAlpha ? 1 : 0);
881
882 try (BinaryOutputStream bos = BinaryOutputStream.littleEndian(os)) {
883
884
885 os.write(0x47);
886 os.write(0x49);
887 os.write(0x46);
888
889 os.write(0x38);
890 os.write(0x39);
891 os.write(0x61);
892
893
894
895 bos.write2Bytes(width);
896 bos.write2Bytes(height);
897
898 final int colorTableScaleLessOne = paletteSize > 128 ? 7
899 : paletteSize > 64 ? 6 : paletteSize > 32 ? 5 : paletteSize > 16 ? 4 : paletteSize > 8 ? 3 : paletteSize > 4 ? 2 : paletteSize > 2 ? 1 : 0;
900
901 final int colorTableSizeInFormat = 1 << colorTableScaleLessOne + 1;
902 {
903 final byte colorResolution = (byte) colorTableScaleLessOne;
904 final int packedFields = (7 & colorResolution) * 16;
905 bos.write(packedFields);
906 }
907 {
908 final byte backgroundColorIndex = 0;
909 bos.write(backgroundColorIndex);
910 }
911 {
912 final byte pixelAspectRatio = 0;
913 bos.write(pixelAspectRatio);
914 }
915
916
917
918
919
920
921 {
922 bos.write(EXTENSION_CODE);
923 bos.write((byte) 0xf9);
924
925
926
927 bos.write((byte) 4);
928 final int packedFields = hasAlpha ? 1 : 0;
929 bos.write((byte) packedFields);
930 bos.write((byte) 0);
931 bos.write((byte) 0);
932 bos.write((byte) (hasAlpha ? palette2.length() : 0));
933
934
935 bos.write((byte) 0);
936 }
937
938 if (null != xmpXml) {
939 bos.write(EXTENSION_CODE);
940 bos.write(APPLICATION_EXTENSION_LABEL);
941
942 bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE.length);
943 bos.write(XMP_APPLICATION_ID_AND_AUTH_CODE);
944
945 final byte[] xmpXmlBytes = xmpXml.getBytes(StandardCharsets.UTF_8);
946 bos.write(xmpXmlBytes);
947
948
949 for (int magic = 0; magic <= 0xff; magic++) {
950 bos.write(0xff - magic);
951 }
952
953 bos.write((byte) 0);
954
955 }
956
957 {
958 bos.write(IMAGE_SEPARATOR);
959 bos.write2Bytes(0);
960 bos.write2Bytes(0);
961 bos.write2Bytes(width);
962 bos.write2Bytes(height);
963
964 {
965 final boolean localColorTableFlag = true;
966
967 final boolean interlaceFlag = false;
968 final boolean sortFlag = false;
969 final int sizeOfLocalColorTable = colorTableScaleLessOne;
970
971
972
973 final int packedFields;
974 if (localColorTableFlag) {
975 packedFields = LOCAL_COLOR_TABLE_FLAG_MASK | (interlaceFlag ? INTERLACE_FLAG_MASK : 0) | (sortFlag ? SORT_FLAG_MASK : 0)
976 | 7 & sizeOfLocalColorTable;
977 } else {
978 packedFields = 0 | (interlaceFlag ? INTERLACE_FLAG_MASK : 0) | (sortFlag ? SORT_FLAG_MASK : 0) | 7 & sizeOfLocalColorTable;
979 }
980 bos.write(packedFields);
981 }
982 }
983
984 {
985 for (int i = 0; i < colorTableSizeInFormat; i++) {
986 if (i < palette2.length()) {
987 final int rgb = palette2.getEntry(i);
988
989 final int red = 0xff & rgb >> 16;
990 final int green = 0xff & rgb >> 8;
991 final int blue = 0xff & rgb >> 0;
992
993 bos.write(red);
994 bos.write(green);
995 bos.write(blue);
996 } else {
997 bos.write(0);
998 bos.write(0);
999 bos.write(0);
1000 }
1001 }
1002 }
1003
1004 {
1005
1006
1007 int lzwMinimumCodeSize = colorTableScaleLessOne + 1;
1008
1009 if (lzwMinimumCodeSize < 2) {
1010 lzwMinimumCodeSize = 2;
1011 }
1012
1013
1014
1015
1016
1017
1018 bos.write(lzwMinimumCodeSize);
1019
1020 final MyLzwCompressor compressor = new MyLzwCompressor(lzwMinimumCodeSize, ByteOrder.LITTLE_ENDIAN, false);
1021
1022
1023 final byte[] imageData = Allocator.byteArray(width * height);
1024 for (int y = 0; y < height; y++) {
1025 for (int x = 0; x < width; x++) {
1026 final int argb = src.getRGB(x, y);
1027 final int rgb = 0xffffff & argb;
1028 int index;
1029
1030 if (hasAlpha) {
1031 final int alpha = 0xff & argb >> 24;
1032 final int alphaThreshold = 255;
1033 if (alpha < alphaThreshold) {
1034 index = palette2.length();
1035 } else {
1036 index = palette2.getPaletteIndex(rgb);
1037 }
1038 } else {
1039 index = palette2.getPaletteIndex(rgb);
1040 }
1041
1042 imageData[y * width + x] = (byte) index;
1043 }
1044 }
1045
1046 final byte[] compressed = compressor.compress(imageData);
1047 writeAsSubBlocks(bos, compressed);
1048
1049 }
1050
1051
1052
1053 bos.write(TERMINATOR_BYTE);
1054
1055 }
1056 os.close();
1057 }
1058 }