GifDecoder.java
25.8 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
package com.yoho.unions.utils;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.awt.image.DataBufferInt;
import java.io.IOException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
/*
* Copyright 2014 Dhyan Blum
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
/**
* <p>
* A decoder capable of processing a GIF data stream to render the graphics
* contained in it. This implementation follows the official <A
* HREF="http://www.w3.org/Graphics/GIF/spec-gif89a.txt">GIF specification</A>.
* </p>
*
* <p>
* Example usage:
* </p>
*
* <p>
*
* <pre>
* final GifImage gifImage = GifDecoder.read(int[] data);
* final int width = gifImage.getWidth();
* final int height = gifImage.getHeight();
* final int frameCount = gifImage.getFrameCount();
* for (int i = 0; i < frameCount; i++) {
* final BufferedImage image = gifImage.getFrame(i);
* final int delay = gif.getDelay(i);
* }
* </pre>
*
* </p>
*
* @author Dhyan Blum
* @version 1.07 October 2014
*
*/
public final class GifDecoder {
static final class BitReader {
private int bitPos; // Next bit to read
private byte[] in; // Data array
// To avoid costly bounds checks, 'in' needs 2 more 0-bytes at the end
private final void init(final byte[] in) {
this.in = in;
bitPos = 0;
}
private final int read(final int bits) {
// Byte indices: (bitPos / 8), (bitPos / 8) + 1, (bitPos / 8) + 2
int i = bitPos >>> 3; // Byte = bit / 8
// Bits we'll shift to the right, AND 7 is the same as MODULO 8
final int rBits = bitPos & 7;
// Byte 0 to 2, AND to get their unsigned values
final int b0 = in[i++] & 0xFF, b1 = in[i++] & 0xFF, b2 = in[i] & 0xFF;
// Glue the bytes together, don't do more shifting than necessary
final int buf = ((b2 << 8 | b1) << 8 | b0) >>> rBits;
bitPos += bits;
return buf & MASK[bits]; // Kill the unwanted higher bits
}
}
static final class CodeTable {
private final int[][] tbl; // Maps codes to lists of colors
private int initTableSize; // Number of colors +2 for CLEAR + EOI
private int initCodeSize; // Initial code size
private int initCodeLimit; // First code limit
private int currCodeSize; // Current code size, maximum is 12 bits
private int nextCode; // Next available code for a new entry
private int nextCodeLimit; // Increase codeSize when nextCode == limit
public CodeTable() {
tbl = new int[4096][1];
}
private final int add(final int[] indices) {
if (nextCode < 4096) {
if (nextCode == nextCodeLimit && currCodeSize < 12) {
currCodeSize++; // Max code size is 12
nextCodeLimit = MASK[currCodeSize]; // 2^currCodeSize - 1
}
tbl[nextCode++] = indices;
}
return currCodeSize;
}
private final int clear() {
currCodeSize = initCodeSize;
nextCodeLimit = initCodeLimit;
nextCode = initTableSize; // Don't recreate table, reset pointer
return currCodeSize;
}
private final void init(final GifFrame fr, final int[] activeColTbl) {
final int numColors = activeColTbl.length;
initCodeSize = fr.firstCodeSize;
initCodeLimit = MASK[initCodeSize]; // 2^initCodeSize - 1
initTableSize = fr.endOfInfoCode + 1;
nextCode = initTableSize;
for (int c = numColors - 1; c >= 0; c--) {
tbl[c][0] = activeColTbl[c]; // Translated color
} // A gap may follow with no colors assigned if numCols < CLEAR
tbl[fr.clearCode] = new int[] { fr.clearCode }; // CLEAR
tbl[fr.endOfInfoCode] = new int[] { fr.endOfInfoCode }; // EOI
// Locate transparent color in code table and set to 0
if (fr.transpColFlag && fr.transpColIndex < numColors) {
tbl[fr.transpColIndex][0] = 0;
}
}
}
final class GifFrame {
// Graphic control extension (optional)
// Disposal: 0=NO_ACTION, 1=NO_DISPOSAL, 2=RESTORE_BG, 3=RESTORE_PREV
private int disposalMethod; // 0-3 as above, 4-7 undefined
private boolean transpColFlag; // 1 Bit
private int delay; // Unsigned, LSByte first, n * 1/100 * s
private int transpColIndex; // 1 Byte
// Image descriptor
private int left; // Position on the canvas from the left
private int top; // Position on the canvas from the top
private int width; // May be smaller than the base image
private int height; // May be smaller than the base image
private boolean hasLocColTbl; // Has local color table? 1 Bit
private boolean interlaceFlag; // Is an interlace image? 1 Bit
@SuppressWarnings("unused")
private boolean sortFlag; // True if local colors are sorted, 1 Bit
private int sizeOfLocColTbl; // Size of the local color table, 3 Bits
private int[] localColTbl; // Local color table (optional)
// Image data
private int firstCodeSize; // LZW minimum code size + 1 for CLEAR & EOI
private int clearCode;
private int endOfInfoCode;
private byte[] data;
}
public final class GifImage {
public String header; // Bytes 0-5, GIF87a or GIF89a
private int width; // Unsigned 16 Bit, least significant byte first
private int height; // Unsigned 16 Bit, least significant byte first
private int wh; // width * height
public boolean hasGlobColTbl; // 1 Bit
public int colorResolution; // 3 Bits
public boolean sortFlag; // True if global colors are sorted, 1 Bit
public int sizeOfGlobColTbl; // 2^(val(3 Bits) + 1), see spec
public int bgColIndex; // Background color index, 1 Byte
public int pxAspectRatio; // Pixel aspect ratio, 1 Byte
public int[] globalColTbl; // Global color table
private final List<GifFrame> frames = new ArrayList<GifFrame>(48);
public String appId = ""; // 8 Bytes at in[i+3], usually "NETSCAPE"
public String appAuthCode = ""; // 3 Bytes at in[i+11], usually "2.0"
public int repetitions = 0; // 0: infinite loop, N: number of loops
private BufferedImage img = null; // Currently drawn frame
private BufferedImage prevImg = null; // Last drawn frame
private int prevIndex; // Index of the last drawn frame
private int prevDisposal; // Disposal of the previous frame
private final BitReader in = new BitReader();
private final CodeTable codes = new CodeTable();
public int[] pxBuffer;
private final int[] decode(final GifFrame fr, final int[] activeColTbl) {
codes.init(fr, activeColTbl);
in.init(fr.data); // Incoming codes
final int clearCode = fr.clearCode, endCode = fr.endOfInfoCode;
final int[] out = pxBuffer; // Target image pixel array
final int[][] tbl = codes.tbl; // Code table
int pxPos = 0; // Next pixel position in the output image array
int currCodeSize = codes.clear(); // Init code table
in.read(currCodeSize); // Skip leading clear code
int code = in.read(currCodeSize); // Read first code
int[] pixels = tbl[code]; // Output pixel for first code
System.arraycopy(pixels, 0, out, pxPos, pixels.length);
pxPos += pixels.length;
try {
while (true) {
final int prevCode = code;
code = in.read(currCodeSize); // Get next code in stream
if (code == clearCode) { // After a CLEAR table, there is
currCodeSize = codes.clear(); // No previous code, we
code = in.read(currCodeSize); // need to read a new one
pixels = tbl[code]; // Output pixels
System.arraycopy(pixels, 0, out, pxPos, pixels.length);
pxPos += pixels.length;
continue; // Back to the loop with a valid previous code
} else if (code == endCode) {
break;
}
final int[] prevVals = tbl[prevCode];
final int[] prevValsAndK = new int[prevVals.length + 1];
System.arraycopy(prevVals, 0, prevValsAndK, 0,
prevVals.length);
if (code < codes.nextCode) { // Code table contains code
pixels = tbl[code]; // Output pixels
System.arraycopy(pixels, 0, out, pxPos, pixels.length);
pxPos += pixels.length;
prevValsAndK[prevVals.length] = tbl[code][0]; // K
} else {
prevValsAndK[prevVals.length] = prevVals[0]; // K
System.arraycopy(prevValsAndK, 0, out, pxPos,
prevValsAndK.length);
pxPos += prevValsAndK.length;
}
currCodeSize = codes.add(prevValsAndK); // Previous indices
// + K
}
} catch (final ArrayIndexOutOfBoundsException e) {
}
return out;
}
private final int[] deinterlace(final int[] pixels, final GifFrame fr) {
final int w = fr.width, h = fr.height, group2, group3, group4;
final int[] dest = new int[pixels.length];
// Interlaced images are divided in 4 groups of pixel lines
group2 = (h + 7) / 8; // Start index of group 2 = ceil(h/8.0)
group3 = group2 + (h + 3) / 8; // Start index = ceil(h-4/8.0)
group4 = group3 + (h + 1) / 4; // Start index = ceil(h-2/4.0)
// Group 1 contains every 8th line starting from 0
for (int y = 0; y < group2; y++) {
final int destPos = w * y * 8;
System.arraycopy(pixels, w * y, dest, destPos, w);
} // Group 2 contains every 8th line starting from 4
for (int y = group2; y < group3; y++) {
final int destY = (y - group2) * 8 + 4, destPos = w * destY;
System.arraycopy(pixels, w * y, dest, destPos, w);
} // Group 3 contains every 4th line starting from 2
for (int y = group3; y < group4; y++) {
final int destY = (y - group3) * 4 + 2, destPos = w * destY;
System.arraycopy(pixels, w * y, dest, destPos, w);
} // Group 4 contains every 2nd line starting from 1 (biggest group)
for (int y = group4; y < h; y++) {
final int destY = (y - group4) * 2 + 1, destPos = w * destY;
System.arraycopy(pixels, w * y, dest, destPos, w);
}
return dest; // All pixel lines have now been rearranged
}
private final void drawFrame(final GifFrame fr) {
int bgCol = 0; // Current background color value
final int[] activeColTbl; // Active color table
if (fr.hasLocColTbl) {
activeColTbl = fr.localColTbl;
} else {
activeColTbl = globalColTbl;
if (!fr.transpColFlag) { // Only use background color if there
bgCol = globalColTbl[bgColIndex]; // is no transparency
}
}
// Handle disposal, prepare current BufferedImage for drawing
switch (prevDisposal) {
case 2: // Next frame draws on background canvas
final BufferedImage bgImage = prevImg;
final int[] px = getPixels(bgImage);
Arrays.fill(px, bgCol); // Set background color of bgImage
prevImg = img; // Let previous point to current, dispose current
img = bgImage; // Let current point to background image
break;
case 3: // Next frame draws on previous frame, so restore previous
System.arraycopy(getPixels(prevImg), 0, getPixels(img), 0, wh);
break;
default: // Next frame draws on current frame, so backup current
System.arraycopy(getPixels(img), 0, getPixels(prevImg), 0, wh);
break;
}
// Get pixels from data stream
int[] pixels = decode(fr, activeColTbl);
if (fr.interlaceFlag) {
pixels = deinterlace(pixels, fr); // Rearrange pixel lines
}
// Draw pixels on top of current image
final int w = fr.width, h = fr.height, numPixels = w * h;
final BufferedImage frame = new BufferedImage(w, h, 2); // 2 = ARGB
System.arraycopy(pixels, 0, getPixels(frame), 0, numPixels);
final Graphics2D g = img.createGraphics();
g.drawImage(frame, fr.left, fr.top, null);
g.dispose();
}
/**
* Returns the background color of the first frame in this GIF image. If
* the frame has a local color table, the returned color will be from
* that table. If not, the color will be from the global color table.
* Returns 0 if there is neither a local nor a global color table.
*
* @return 32 bit ARGB color in the form 0xAARRGGBB
*/
public final int getBackgroundColor() {
final GifFrame frame = frames.get(0);
if (frame.hasLocColTbl) {
return frame.localColTbl[bgColIndex];
} else if (hasGlobColTbl) {
return globalColTbl[bgColIndex];
}
return 0;
}
/**
* If not 0, the delay specifies how many hundredths (1/100) of a second
* to wait before displaying the frame <i>after</i> the current frame.
*
* @param index
* Index of the current frame, 0 to N-1
* @return Delay as number of hundredths (1/100) of a second
*/
public final int getDelay(final int index) {
return frames.get(index).delay;
}
/**
* @param index
* Index of the frame to return as image, starting from 0.
* For indices greater than 0, it may be necessary to draw
* the current frame on top of its previous frames. This
* behavior depends on the disposal method encoded in the
* image. This method's runtime therefore increases with
* higher indices. However, the runtime increase will be
* linear for calls with ascending indices. So don't request
* frame i after requesting frame i+n, as all frames prior to
* i might have to be redrawn. Use call sequences with
* indices from 0 to N-1 instead.
* @return A BufferedImage for the specified frame.
*/
public final BufferedImage getFrame(final int index) {
if (img == null || index < prevIndex) { // (Re)Init
img = new BufferedImage(width, height, 2); // 2 = ARGB
prevImg = new BufferedImage(width, height, 2);
prevIndex = -1;
prevDisposal = 2;
}
// Draw current frame on top of previous frames
for (int i = prevIndex + 1; i <= index; i++) {
final GifFrame fr = frames.get(i);
drawFrame(fr);
prevIndex = i;
prevDisposal = fr.disposalMethod;
}
return img;
}
/**
* @return The number of frames contained in this GIF image
*/
public final int getFrameCount() {
return frames.size();
}
/**
* @return The height of the GIF image
*/
public final int getHeight() {
return height;
}
private final int[] getPixels(final BufferedImage img) {
return ((DataBufferInt) img.getRaster().getDataBuffer()).getData();
}
/**
* @return The width of the GIF image
*/
public final int getWidth() {
return width;
}
}
// If used as a bitmask, the index tells how much lower bits remain.
// May also be used to compute f(i) = (2^i) - 1.
static final int[] MASK = new int[] { 0x00000000, 0x00000001, 0x00000003,
0x00000007, 0x0000000F, 0x0000001F, 0x0000003F, 0x0000007F,
0x000000FF, 0x000001FF, 0x000003FF, 0x000007FF, 0x00000FFF,
0x00001FFF, 0x00003FFF, 0x00007FFF, 0x0000FFFF, 0x0001FFFF,
0x0003FFFF, 0x0007FFFF, 0x000FFFFF, 0x001FFFFF, 0x003FFFFF,
0x007FFFFF, 0x00FFFFFF, 0x01FFFFFF, 0x03FFFFFF, 0x07FFFFFF,
0x0FFFFFFF, 0x1FFFFFFF, 0x3FFFFFFF, 0x7FFFFFFF, 0xFFFFFFFF };
/**
* @param in
* Raw image data as a byte[] array
* @return A GifImage object exposing the properties of the GIF image.
* @throws IOException
* If the image violates the GIF specification or is truncated.
*/
public static final GifImage read(final byte[] in) throws IOException {
final GifDecoder dec = new GifDecoder();
final GifImage img = dec.new GifImage();
final List<GifFrame> frames = img.frames; // Local access is faster
GifFrame frame = null; // Currently open frame
int pos = readHeader(in, img); // Read header, get next byte position
pos = readLogicalScreenDescriptor(img, in, pos);
if (img.hasGlobColTbl) {
img.globalColTbl = new int[img.sizeOfGlobColTbl];
pos = readColTbl(in, img.globalColTbl, pos);
}
while (pos < in.length) {
final int block = in[pos] & 0xFF;
switch (block) {
case 0x21: // Extension introducer
if (pos + 1 >= in.length) {
throw new IOException("Unexpected end of file.");
}
switch (in[pos + 1] & 0xFF) {
case 0xFE: // Comment extension
pos = readTextExtension(in, pos);
break;
case 0xFF: // Application extension
pos = readAppExt(img, in, pos);
break;
case 0x01: // Plain text extension
frame = null; // End of current frame
pos = readTextExtension(in, pos);
break;
case 0xF9: // Graphic control extension
if (frame == null) {
frame = dec.new GifFrame();
frames.add(frame);
}
pos = readGraphicControlExt(frame, in, pos);
break;
default:
throw new IOException("Unknown extension at " + pos);
}
break;
case 0x2C: // Image descriptor
if (frame == null) {
frame = dec.new GifFrame();
frames.add(frame);
}
pos = readImgDescr(frame, in, pos);
if (frame.hasLocColTbl) {
frame.localColTbl = new int[frame.sizeOfLocColTbl];
pos = readColTbl(in, frame.localColTbl, pos);
}
pos = readImgData(frame, in, pos);
frame = null; // End of current frame
break;
case 0x3B: // GIF Trailer
return img; // Found trailer, finished reading.
default:
// Unknown block. The image is corrupted. Strategies: a) Skip
// and wait for a valid block. Experience: It'll get worse. b)
// Throw exception. c) Return gracefully if we are almost done
// processing. The frames we have so far should be error-free.
final double progress = 1.0 * pos / in.length;
if (progress < 0.9) {
throw new IOException("Unknown block at: " + pos);
}
pos = in.length; // Exit loop
}
}
return img;
}
/**
* @param is
* Image data as input stream. This method will read from the
* input stream's current position. It will not reset the
* position before reading and won't reset or close the stream
* afterwards. Call these methods before and after calling this
* method as needed.
* @return A GifImage object exposing the properties of the GIF image.
* @throws IOException
* If an I/O error occurs, the image violates the GIF
* specification or the GIF is truncated.
*/
public static final GifImage read(final InputStream is) throws IOException {
final int numBytes = is.available();
final byte[] data = new byte[numBytes];
is.read(data, 0, numBytes);
return read(data);
}
/**
* @param img
* Empty application extension object
* @param in
* Raw data
* @param i
* Index of the first byte of the application extension
* @return Index of the first byte after this extension
*/
static final int readAppExt(final GifImage img, final byte[] in, int i) {
img.appId = new String(in, i + 3, 8); // should be "NETSCAPE"
img.appAuthCode = new String(in, i + 11, 3); // should be "2.0"
i += 14; // Go to sub-block size, it's value should be 3
final int subBlockSize = in[i] & 0xFF;
// The only app extension widely used is NETSCAPE, it's got 3 data bytes
if (subBlockSize == 3) {
// in[i+1] should have value 01, in[i+5] should be block terminator
img.repetitions = in[i + 2] & 0xFF | in[i + 3] & 0xFF << 8; // Short
return i + 5;
} // Skip unknown application extensions
while ((in[i] & 0xFF) != 0) { // While sub-block size != 0
i += (in[i] & 0xFF) + 1; // Skip to next sub-block
}
return i + 1;
}
/**
* @param in
* Raw data
* @param colors
* Pre-initialized target array to store ARGB colors
* @param i
* Index of the color table's first byte
* @return Index of the first byte after the color table
*/
static final int readColTbl(final byte[] in, final int[] colors, int i) {
final int numColors = colors.length;
for (int c = 0; c < numColors; c++) {
final int a = 0xFF; // Alpha 255 (opaque)
final int r = in[i++] & 0xFF; // 1st byte is red
final int g = in[i++] & 0xFF; // 2nd byte is green
final int b = in[i++] & 0xFF; // 3rd byte is blue
colors[c] = ((a << 8 | r) << 8 | g) << 8 | b;
}
return i;
}
/**
* @param fr
* Graphic control extension object
* @param in
* Raw data
* @param i
* Index of the extension introducer
* @return Index of the first byte after this block
*/
static final int readGraphicControlExt(final GifFrame fr, final byte[] in,
final int i) {
fr.disposalMethod = (in[i + 3] & 0b00011100) >>> 2; // Bits 4-2
fr.transpColFlag = (in[i + 3] & 1) == 1; // Bit 0
fr.delay = in[i + 4] & 0xFF | (in[i + 5] & 0xFF) << 8; // 16 bit LSB
fr.transpColIndex = in[i + 6] & 0xFF; // Byte 6
return i + 8; // Skipped byte 7 (blockTerminator), as it's always 0x00
}
/**
* @param in
* Raw data
* @param img
* The GifImage object that is currently read
* @return Index of the first byte after this block
* @throws IOException
* If the GIF header/trailer is missing, incomplete or unknown
*/
static final int readHeader(final byte[] in, final GifImage img)
throws IOException {
if (in.length < 6) { // Check first 6 bytes
throw new IOException("Image is truncated.");
}
img.header = new String(in, 0, 6);
if (!img.header.equals("GIF87a") && !img.header.equals("GIF89a")) {
throw new IOException("Invalid GIF header.");
}
return 6;
}
/**
* @param fr
* The GIF frame to whom this image descriptor belongs
* @param in
* Raw data
* @param i
* Index of the first byte of this block, i.e. the minCodeSize
* @return
*/
static final int readImgData(final GifFrame fr, final byte[] in, int i) {
final int fileSize = in.length;
final int minCodeSize = in[i++] & 0xFF; // Read code size, go to block
final int clearCode = 1 << minCodeSize; // CLEAR = 2^minCodeSize
fr.firstCodeSize = minCodeSize + 1; // Add 1 bit for CLEAR and EOI
fr.clearCode = clearCode;
fr.endOfInfoCode = clearCode + 1;
final int imgDataSize = readImgDataSize(in, i);
final byte[] imgData = new byte[imgDataSize + 2];
int imgDataPos = 0;
int subBlockSize = in[i] & 0xFF;
while (subBlockSize > 0) { // While block has data
try { // Next line may throw exception if sub-block size is fake
final int nextSubBlockSizePos = i + subBlockSize + 1;
final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
imgDataPos += subBlockSize; // Move output data position
i = nextSubBlockSizePos; // Move to next sub-block size
subBlockSize = nextSubBlockSize;
} catch (final Exception e) {
// Sub-block exceeds file end, only use remaining bytes
subBlockSize = fileSize - i - 1; // Remaining bytes
System.arraycopy(in, i + 1, imgData, imgDataPos, subBlockSize);
imgDataPos += subBlockSize; // Move output data position
i += subBlockSize + 1; // Move to next sub-block size
break;
}
}
fr.data = imgData; // Holds LZW encoded data
i++; // Skip last sub-block size, should be 0
return i;
}
static final int readImgDataSize(final byte[] in, int i) {
final int fileSize = in.length;
int imgDataPos = 0;
int subBlockSize = in[i] & 0xFF;
while (subBlockSize > 0) { // While block has data
try { // Next line may throw exception if sub-block size is fake
final int nextSubBlockSizePos = i + subBlockSize + 1;
final int nextSubBlockSize = in[nextSubBlockSizePos] & 0xFF;
imgDataPos += subBlockSize; // Move output data position
i = nextSubBlockSizePos; // Move to next sub-block size
subBlockSize = nextSubBlockSize;
} catch (final Exception e) {
// Sub-block exceeds file end, only use remaining bytes
subBlockSize = fileSize - i - 1; // Remaining bytes
imgDataPos += subBlockSize; // Move output data position
break;
}
}
return imgDataPos;
}
/**
* @param fr
* The GIF frame to whom this image descriptor belongs
* @param in
* Raw data
* @param i
* Index of the image separator, i.e. the first block byte
* @return Index of the first byte after this block
*/
static final int readImgDescr(final GifFrame fr, final byte[] in, int i) {
fr.left = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 1-2
fr.top = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 3-4
fr.width = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 5-6
fr.height = in[++i] & 0xFF | (in[++i] & 0xFF) << 8; // Byte 7-8
final byte b = in[++i]; // Byte 9 is a packed byte
fr.hasLocColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7
fr.interlaceFlag = (b & 0b01000000) >>> 6 == 1; // Bit 6
fr.sortFlag = (b & 0b00100000) >>> 5 == 1; // Bit 5
final int colTblSizePower = (b & 7) + 1; // Bits 2-0
fr.sizeOfLocColTbl = 1 << colTblSizePower; // 2^(N+1), As per the spec
return ++i;
}
/**
* @param img
* @param i
* Start index of this block.
* @return Index of the first byte after this block.
*/
static final int readLogicalScreenDescriptor(final GifImage img,
final byte[] in, final int i) {
img.width = in[i] & 0xFF | (in[i + 1] & 0xFF) << 8; // 16 bit, LSB 1st
img.height = in[i + 2] & 0xFF | (in[i + 3] & 0xFF) << 8; // 16 bit
img.wh = img.width * img.height;
img.pxBuffer = new int[img.wh]; // Output pixel buffer
final byte b = in[i + 4]; // Byte 4 is a packed byte
img.hasGlobColTbl = (b & 0b10000000) >>> 7 == 1; // Bit 7
final int colResPower = ((b & 0b01110000) >>> 4) + 1; // Bits 6-4
img.colorResolution = 1 << colResPower; // 2^(N+1), As per the spec
img.sortFlag = (b & 0b00001000) >>> 3 == 1; // Bit 3
final int globColTblSizePower = (b & 7) + 1; // Bits 0-2
img.sizeOfGlobColTbl = 1 << globColTblSizePower; // 2^(N+1), see spec
img.bgColIndex = in[i + 5] & 0xFF; // 1 Byte
img.pxAspectRatio = in[i + 6] & 0xFF; // 1 Byte
return i + 7;
}
/**
* @param in
* Raw data
* @param pos
* Index of the extension introducer
* @return Index of the first byte after this block
*/
static final int readTextExtension(final byte[] in, final int pos) {
int i = pos + 2; // Skip extension introducer and label
int subBlockSize = in[i++] & 0xFF;
while (subBlockSize != 0 && i < in.length) {
i += subBlockSize;
subBlockSize = in[i++] & 0xFF;
}
return i;
}
}