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