Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
epoxy
GitHub Repository: epoxy/proj11
Path: blob/master/SLICK_HOME/src/org/newdawn/slick/opengl/PNGImageData.java
1461 views
1
package org.newdawn.slick.opengl;
2
3
import java.io.EOFException;
4
import java.io.IOException;
5
import java.io.InputStream;
6
import java.nio.ByteBuffer;
7
import java.util.zip.CRC32;
8
import java.util.zip.DataFormatException;
9
import java.util.zip.Inflater;
10
11
import org.lwjgl.BufferUtils;
12
13
/**
14
* The PNG imge data source that is pure java reading PNGs
15
*
16
* @author Matthias Mann (original code)
17
*/
18
public class PNGImageData implements LoadableImageData {
19
/** The valid signature of a PNG */
20
private static final byte[] SIGNATURE = {(byte)137, 80, 78, 71, 13, 10, 26, 10};
21
22
/** The header chunk identifer */
23
private static final int IHDR = 0x49484452;
24
/** The palette chunk identifer */
25
private static final int PLTE = 0x504C5445;
26
/** The transparency chunk identifier */
27
private static final int tRNS = 0x74524E53;
28
/** The data chunk identifier */
29
private static final int IDAT = 0x49444154;
30
/** The end chunk identifier */
31
private static final int IEND = 0x49454E44;
32
33
/** Color type for greyscale images */
34
private static final byte COLOR_GREYSCALE = 0;
35
/** Color type for true colour images */
36
private static final byte COLOR_TRUECOLOR = 2;
37
/** Color type for indexed palette images */
38
private static final byte COLOR_INDEXED = 3;
39
/** Color type for greyscale images with alpha */
40
private static final byte COLOR_GREYALPHA = 4;
41
/** Color type for true colour images with alpha */
42
private static final byte COLOR_TRUEALPHA = 6;
43
44
/** The stream we're going to read from */
45
private InputStream input;
46
/** The CRC for the current chunk */
47
private final CRC32 crc;
48
/** The buffer we'll use as temporary storage */
49
private final byte[] buffer;
50
51
/** The length of the current chunk in bytes */
52
private int chunkLength;
53
/** The ID of the current chunk */
54
private int chunkType;
55
/** The number of bytes remaining in the current chunk */
56
private int chunkRemaining;
57
58
/** The width of the image read */
59
private int width;
60
/** The height of the image read */
61
private int height;
62
/** The type of colours in the PNG data */
63
private int colorType;
64
/** The number of bytes per pixel */
65
private int bytesPerPixel;
66
/** The palette data that has been read - RGB only */
67
private byte[] palette;
68
/** The palette data thats be read from alpha channel */
69
private byte[] paletteA;
70
/** The transparent pixel description */
71
private byte[] transPixel;
72
73
/** The bit depth of the image */
74
private int bitDepth;
75
/** The width of the texture to be generated */
76
private int texWidth;
77
/** The height of the texture to be generated */
78
private int texHeight;
79
80
/** The scratch buffer used to store the image data */
81
private ByteBuffer scratch;
82
83
/**
84
* Create a new PNG image data that can read image data from PNG formated files
85
*/
86
public PNGImageData() {
87
this.crc = new CRC32();
88
this.buffer = new byte[4096];
89
}
90
91
/**
92
* Initialise the PNG data header fields from the input stream
93
*
94
* @param input The input stream to read from
95
* @throws IOException Indicates a failure to read appropriate data from the stream
96
*/
97
private void init(InputStream input) throws IOException {
98
this.input = input;
99
100
int read = input.read(buffer, 0, SIGNATURE.length);
101
if(read != SIGNATURE.length || !checkSignatur(buffer)) {
102
throw new IOException("Not a valid PNG file");
103
}
104
105
openChunk(IHDR);
106
readIHDR();
107
closeChunk();
108
109
searchIDAT: for(;;) {
110
openChunk();
111
switch (chunkType) {
112
case IDAT:
113
break searchIDAT;
114
case PLTE:
115
readPLTE();
116
break;
117
case tRNS:
118
readtRNS();
119
break;
120
}
121
closeChunk();
122
}
123
}
124
125
/**
126
* @see org.newdawn.slick.opengl.ImageData#getHeight()
127
*/
128
public int getHeight() {
129
return height;
130
}
131
132
/**
133
* @see org.newdawn.slick.opengl.ImageData#getWidth()
134
*/
135
public int getWidth() {
136
return width;
137
}
138
139
/**
140
* Check if this PNG has a an alpha channel
141
*
142
* @return True if the PNG has an alpha channel
143
*/
144
public boolean hasAlpha() {
145
return colorType == COLOR_TRUEALPHA ||
146
paletteA != null || transPixel != null;
147
}
148
149
/**
150
* Check if the PNG is RGB formatted
151
*
152
* @return True if the PNG is RGB formatted
153
*/
154
public boolean isRGB() {
155
return colorType == COLOR_TRUEALPHA ||
156
colorType == COLOR_TRUECOLOR ||
157
colorType == COLOR_INDEXED;
158
}
159
160
/**
161
* Decode a PNG into a data buffer
162
*
163
* @param buffer The buffer to read the data into
164
* @param stride The image stride to read (i.e. the number of bytes to skip each line)
165
* @param flip True if the PNG should be flipped
166
* @throws IOException Indicates a failure to read the PNG either invalid data or
167
* not enough room in the buffer
168
*/
169
private void decode(ByteBuffer buffer, int stride, boolean flip) throws IOException {
170
final int offset = buffer.position();
171
byte[] curLine = new byte[width*bytesPerPixel+1];
172
byte[] prevLine = new byte[width*bytesPerPixel+1];
173
174
final Inflater inflater = new Inflater();
175
try {
176
for(int yIndex=0 ; yIndex<height ; yIndex++) {
177
int y = yIndex;
178
if (flip) {
179
y = height - 1 - yIndex;
180
}
181
182
readChunkUnzip(inflater, curLine, 0, curLine.length);
183
unfilter(curLine, prevLine);
184
185
buffer.position(offset + y*stride);
186
187
switch (colorType) {
188
case COLOR_TRUECOLOR:
189
case COLOR_TRUEALPHA:
190
copy(buffer, curLine);
191
break;
192
case COLOR_INDEXED:
193
copyExpand(buffer, curLine);
194
break;
195
default:
196
throw new UnsupportedOperationException("Not yet implemented");
197
}
198
199
byte[] tmp = curLine;
200
curLine = prevLine;
201
prevLine = tmp;
202
}
203
} finally {
204
inflater.end();
205
}
206
207
bitDepth = hasAlpha() ? 32 : 24;
208
}
209
210
/**
211
* Copy some data into the given byte buffer expanding the
212
* data based on indexing the palette
213
*
214
* @param buffer The buffer to write into
215
* @param curLine The current line of data to copy
216
*/
217
private void copyExpand(ByteBuffer buffer, byte[] curLine) {
218
for (int i=1;i<curLine.length;i++) {
219
int v = curLine[i] & 255;
220
221
int index = v * 3;
222
for (int j=0;j<3;j++) {
223
buffer.put(palette[index+j]);
224
}
225
226
if (hasAlpha()) {
227
if (paletteA != null) {
228
buffer.put(paletteA[v]);
229
} else {
230
buffer.put((byte) 255);
231
}
232
}
233
}
234
}
235
236
/**
237
* Copy the data given directly into the byte buffer (skipping
238
* the filter byte);
239
*
240
* @param buffer The buffer to write into
241
* @param curLine The current line to copy into the buffer
242
*/
243
private void copy(ByteBuffer buffer, byte[] curLine) {
244
buffer.put(curLine, 1, curLine.length-1);
245
}
246
247
/**
248
* Unfilter the data, i.e. convert it back to it's original form
249
*
250
* @param curLine The line of data just read
251
* @param prevLine The line before
252
* @throws IOException Indicates a failure to unfilter the data due to an unknown
253
* filter type
254
*/
255
private void unfilter(byte[] curLine, byte[] prevLine) throws IOException {
256
switch (curLine[0]) {
257
case 0: // none
258
break;
259
case 1:
260
unfilterSub(curLine);
261
break;
262
case 2:
263
unfilterUp(curLine, prevLine);
264
break;
265
case 3:
266
unfilterAverage(curLine, prevLine);
267
break;
268
case 4:
269
unfilterPaeth(curLine, prevLine);
270
break;
271
default:
272
throw new IOException("invalide filter type in scanline: " + curLine[0]);
273
}
274
}
275
276
/**
277
* Sub unfilter
278
* {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html}
279
*
280
* @param curLine The line of data to be unfiltered
281
*/
282
private void unfilterSub(byte[] curLine) {
283
final int bpp = this.bytesPerPixel;
284
final int lineSize = width*bpp;
285
286
for(int i=bpp+1 ; i<=lineSize ; ++i) {
287
curLine[i] += curLine[i-bpp];
288
}
289
}
290
291
/**
292
* Up unfilter
293
* {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html}
294
*
295
* @param prevLine The line of data read before the current
296
* @param curLine The line of data to be unfiltered
297
*/
298
private void unfilterUp(byte[] curLine, byte[] prevLine) {
299
final int bpp = this.bytesPerPixel;
300
final int lineSize = width*bpp;
301
302
for(int i=1 ; i<=lineSize ; ++i) {
303
curLine[i] += prevLine[i];
304
}
305
}
306
307
/**
308
* Average unfilter
309
* {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html}
310
*
311
* @param prevLine The line of data read before the current
312
* @param curLine The line of data to be unfiltered
313
*/
314
private void unfilterAverage(byte[] curLine, byte[] prevLine) {
315
final int bpp = this.bytesPerPixel;
316
final int lineSize = width*bpp;
317
318
int i;
319
for(i=1 ; i<=bpp ; ++i) {
320
curLine[i] += (byte)((prevLine[i] & 0xFF) >>> 1);
321
}
322
for(; i<=lineSize ; ++i) {
323
curLine[i] += (byte)(((prevLine[i] & 0xFF) + (curLine[i - bpp] & 0xFF)) >>> 1);
324
}
325
}
326
327
/**
328
* Paeth unfilter
329
* {@url http://libpng.nigilist.ru/pub/png/spec/1.2/PNG-Filters.html}
330
*
331
* @param prevLine The line of data read before the current
332
* @param curLine The line of data to be unfiltered
333
*/
334
private void unfilterPaeth(byte[] curLine, byte[] prevLine) {
335
final int bpp = this.bytesPerPixel;
336
final int lineSize = width*bpp;
337
338
int i;
339
for(i=1 ; i<=bpp ; ++i) {
340
curLine[i] += prevLine[i];
341
}
342
for(; i<=lineSize ; ++i) {
343
int a = curLine[i - bpp] & 255;
344
int b = prevLine[i] & 255;
345
int c = prevLine[i - bpp] & 255;
346
int p = a + b - c;
347
int pa = p - a; if(pa < 0) pa = -pa;
348
int pb = p - b; if(pb < 0) pb = -pb;
349
int pc = p - c; if(pc < 0) pc = -pc;
350
if(pa<=pb && pa<=pc)
351
c = a;
352
else if(pb<=pc)
353
c = b;
354
curLine[i] += (byte)c;
355
}
356
}
357
358
/**
359
* Read the header of the PNG
360
*
361
* @throws IOException Indicates a failure to read the header
362
*/
363
private void readIHDR() throws IOException {
364
checkChunkLength(13);
365
readChunk(buffer, 0, 13);
366
width = readInt(buffer, 0);
367
height = readInt(buffer, 4);
368
369
if(buffer[8] != 8) {
370
throw new IOException("Unsupported bit depth");
371
}
372
373
colorType = buffer[9] & 255;
374
switch (colorType) {
375
case COLOR_GREYSCALE:
376
bytesPerPixel = 1;
377
break;
378
case COLOR_TRUECOLOR:
379
bytesPerPixel = 3;
380
break;
381
case COLOR_TRUEALPHA:
382
bytesPerPixel = 4;
383
break;
384
case COLOR_INDEXED:
385
bytesPerPixel = 1;
386
break;
387
default:
388
throw new IOException("unsupported color format");
389
}
390
391
if(buffer[10] != 0) {
392
throw new IOException("unsupported compression method");
393
}
394
if(buffer[11] != 0) {
395
throw new IOException("unsupported filtering method");
396
}
397
if(buffer[12] != 0) {
398
throw new IOException("unsupported interlace method");
399
}
400
}
401
402
/**
403
* Read the palette chunk
404
*
405
* @throws IOException Indicates a failure to fully read the chunk
406
*/
407
private void readPLTE() throws IOException {
408
int paletteEntries = chunkLength / 3;
409
if(paletteEntries < 1 || paletteEntries > 256 || (chunkLength % 3) != 0) {
410
throw new IOException("PLTE chunk has wrong length");
411
}
412
413
palette = new byte[paletteEntries*3];
414
readChunk(palette, 0, palette.length);
415
}
416
417
/**
418
* Read the transparency chunk
419
*
420
* @throws IOException Indicates a failure to fully read the chunk
421
*/
422
private void readtRNS() throws IOException {
423
switch (colorType) {
424
case COLOR_GREYSCALE:
425
checkChunkLength(2);
426
transPixel = new byte[2];
427
readChunk(transPixel, 0, 2);
428
break;
429
case COLOR_TRUECOLOR:
430
checkChunkLength(6);
431
transPixel = new byte[6];
432
readChunk(transPixel, 0, 6);
433
break;
434
case COLOR_INDEXED:
435
if(palette == null) {
436
throw new IOException("tRNS chunk without PLTE chunk");
437
}
438
paletteA = new byte[palette.length/3];
439
// initialise default palette values
440
for (int i=0;i<paletteA.length;i++) {
441
paletteA[i] = (byte) 255;
442
}
443
readChunk(paletteA, 0, paletteA.length);
444
break;
445
default:
446
// just ignore it
447
}
448
}
449
450
/**
451
* Close the current chunk, skip the remaining data
452
*
453
* @throws IOException Indicates a failure to read off redundant data
454
*/
455
private void closeChunk() throws IOException {
456
if(chunkRemaining > 0) {
457
// just skip the rest and the CRC
458
input.skip(chunkRemaining+4);
459
} else {
460
readFully(buffer, 0, 4);
461
int expectedCrc = readInt(buffer, 0);
462
int computedCrc = (int)crc.getValue();
463
if(computedCrc != expectedCrc) {
464
throw new IOException("Invalid CRC");
465
}
466
}
467
chunkRemaining = 0;
468
chunkLength = 0;
469
chunkType = 0;
470
}
471
472
/**
473
* Open the next chunk, determine the type and setup the internal state
474
*
475
* @throws IOException Indicates a failure to determine chunk information from the stream
476
*/
477
private void openChunk() throws IOException {
478
readFully(buffer, 0, 8);
479
chunkLength = readInt(buffer, 0);
480
chunkType = readInt(buffer, 4);
481
chunkRemaining = chunkLength;
482
crc.reset();
483
crc.update(buffer, 4, 4); // only chunkType
484
}
485
486
/**
487
* Open a chunk of an expected type
488
*
489
* @param expected The expected type of the next chunk
490
* @throws IOException Indicate a failure to read data or a different chunk on the stream
491
*/
492
private void openChunk(int expected) throws IOException {
493
openChunk();
494
if(chunkType != expected) {
495
throw new IOException("Expected chunk: " + Integer.toHexString(expected));
496
}
497
}
498
499
/**
500
* Check the current chunk has the correct size
501
*
502
* @param expected The expected size of the chunk
503
* @throws IOException Indicate an invalid size
504
*/
505
private void checkChunkLength(int expected) throws IOException {
506
if(chunkLength != expected) {
507
throw new IOException("Chunk has wrong size");
508
}
509
}
510
511
/**
512
* Read some data from the current chunk
513
*
514
* @param buffer The buffer to read into
515
* @param offset The offset into the buffer to read into
516
* @param length The amount of data to read
517
* @return The number of bytes read from the chunk
518
* @throws IOException Indicate a failure to read the appropriate data from the chunk
519
*/
520
private int readChunk(byte[] buffer, int offset, int length) throws IOException {
521
if(length > chunkRemaining) {
522
length = chunkRemaining;
523
}
524
readFully(buffer, offset, length);
525
crc.update(buffer, offset, length);
526
chunkRemaining -= length;
527
return length;
528
}
529
530
/**
531
* Refill the inflating stream with data from the stream
532
*
533
* @param inflater The inflater to fill
534
* @throws IOException Indicates there is no more data left or invalid data has been found on
535
* the stream.
536
*/
537
private void refillInflater(Inflater inflater) throws IOException {
538
while(chunkRemaining == 0) {
539
closeChunk();
540
openChunk(IDAT);
541
}
542
int read = readChunk(buffer, 0, buffer.length);
543
inflater.setInput(buffer, 0, read);
544
}
545
546
/**
547
* Read a chunk from the inflater
548
*
549
* @param inflater The inflater to read the data from
550
* @param buffer The buffer to write into
551
* @param offset The offset into the buffer at which to start writing
552
* @param length The number of bytes to read
553
* @throws IOException Indicates a failure to read the complete chunk
554
*/
555
private void readChunkUnzip(Inflater inflater, byte[] buffer, int offset, int length) throws IOException {
556
try {
557
do {
558
int read = inflater.inflate(buffer, offset, length);
559
if(read <= 0) {
560
if(inflater.finished()) {
561
throw new EOFException();
562
}
563
if(inflater.needsInput()) {
564
refillInflater(inflater);
565
} else {
566
throw new IOException("Can't inflate " + length + " bytes");
567
}
568
} else {
569
offset += read;
570
length -= read;
571
}
572
} while(length > 0);
573
} catch (DataFormatException ex) {
574
IOException io = new IOException("inflate error");
575
io.initCause(ex);
576
577
throw io;
578
}
579
}
580
581
/**
582
* Read a complete buffer of data from the input stream
583
*
584
* @param buffer The buffer to read into
585
* @param offset The offset to start copying into
586
* @param length The length of bytes to read
587
* @throws IOException Indicates a failure to access the data
588
*/
589
private void readFully(byte[] buffer, int offset, int length) throws IOException {
590
do {
591
int read = input.read(buffer, offset, length);
592
if(read < 0) {
593
throw new EOFException();
594
}
595
offset += read;
596
length -= read;
597
} while(length > 0);
598
}
599
600
/**
601
* Read an int from a buffer
602
*
603
* @param buffer The buffer to read from
604
* @param offset The offset into the buffer to read from
605
* @return The int read interpreted in big endian
606
*/
607
private int readInt(byte[] buffer, int offset) {
608
return
609
((buffer[offset ] ) << 24) |
610
((buffer[offset+1] & 255) << 16) |
611
((buffer[offset+2] & 255) << 8) |
612
((buffer[offset+3] & 255) );
613
}
614
615
/**
616
* Check the signature of the PNG to confirm it's a PNG
617
*
618
* @param buffer The buffer to read from
619
* @return True if the PNG signature is correct
620
*/
621
private boolean checkSignatur(byte[] buffer) {
622
for(int i=0 ; i<SIGNATURE.length ; i++) {
623
if(buffer[i] != SIGNATURE[i]) {
624
return false;
625
}
626
}
627
return true;
628
}
629
630
/**
631
* @see org.newdawn.slick.opengl.ImageData#getDepth()
632
*/
633
public int getDepth() {
634
return bitDepth;
635
}
636
637
/**
638
* @see org.newdawn.slick.opengl.ImageData#getImageBufferData()
639
*/
640
public ByteBuffer getImageBufferData() {
641
return scratch;
642
}
643
644
/**
645
* @see org.newdawn.slick.opengl.ImageData#getTexHeight()
646
*/
647
public int getTexHeight() {
648
return texHeight;
649
}
650
651
/**
652
* @see org.newdawn.slick.opengl.ImageData#getTexWidth()
653
*/
654
public int getTexWidth() {
655
return texWidth;
656
}
657
658
/**
659
* @see org.newdawn.slick.opengl.LoadableImageData#loadImage(java.io.InputStream)
660
*/
661
public ByteBuffer loadImage(InputStream fis) throws IOException {
662
return loadImage(fis, false, null);
663
}
664
665
/**
666
* @see org.newdawn.slick.opengl.LoadableImageData#loadImage(java.io.InputStream, boolean, int[])
667
*/
668
public ByteBuffer loadImage(InputStream fis, boolean flipped, int[] transparent) throws IOException {
669
return loadImage(fis, flipped, false, transparent);
670
}
671
672
/**
673
* @see org.newdawn.slick.opengl.LoadableImageData#loadImage(java.io.InputStream, boolean, boolean, int[])
674
*/
675
public ByteBuffer loadImage(InputStream fis, boolean flipped, boolean forceAlpha, int[] transparent) throws IOException {
676
if (transparent != null) {
677
forceAlpha = true;
678
}
679
680
init(fis);
681
682
if (!isRGB()) {
683
throw new IOException("Only RGB formatted images are supported by the PNGLoader");
684
}
685
686
texWidth = get2Fold(width);
687
texHeight = get2Fold(height);
688
689
int perPixel = hasAlpha() ? 4 : 3;
690
691
// Get a pointer to the image memory
692
scratch = BufferUtils.createByteBuffer(texWidth * texHeight * perPixel);
693
decode(scratch, texWidth * perPixel, flipped);
694
695
if (height < texHeight-1) {
696
int topOffset = (texHeight-1) * (texWidth*perPixel);
697
int bottomOffset = (height-1) * (texWidth*perPixel);
698
for (int x=0;x<texWidth;x++) {
699
for (int i=0;i<perPixel;i++) {
700
scratch.put(topOffset+x+i, scratch.get(x+i));
701
scratch.put(bottomOffset+(texWidth*perPixel)+x+i, scratch.get(bottomOffset+x+i));
702
}
703
}
704
}
705
if (width < texWidth-1) {
706
for (int y=0;y<texHeight;y++) {
707
for (int i=0;i<perPixel;i++) {
708
scratch.put(((y+1)*(texWidth*perPixel))-perPixel+i, scratch.get(y*(texWidth*perPixel)+i));
709
scratch.put((y*(texWidth*perPixel))+(width*perPixel)+i, scratch.get((y*(texWidth*perPixel))+((width-1)*perPixel)+i));
710
}
711
}
712
}
713
714
if (!hasAlpha() && forceAlpha) {
715
ByteBuffer temp = BufferUtils.createByteBuffer(texWidth * texHeight * 4);
716
for (int x=0;x<texWidth;x++) {
717
for (int y=0;y<texHeight;y++) {
718
int srcOffset = (y*3)+(x*texHeight*3);
719
int dstOffset = (y*4)+(x*texHeight*4);
720
721
temp.put(dstOffset, scratch.get(srcOffset));
722
temp.put(dstOffset+1, scratch.get(srcOffset+1));
723
temp.put(dstOffset+2, scratch.get(srcOffset+2));
724
temp.put(dstOffset+3, (byte) 255);
725
}
726
}
727
728
colorType = COLOR_TRUEALPHA;
729
bitDepth = 32;
730
scratch = temp;
731
}
732
733
if (transparent != null) {
734
for (int i=0;i<texWidth*texHeight*4;i+=4) {
735
boolean match = true;
736
for (int c=0;c<3;c++) {
737
if (toInt(scratch.get(i+c)) != transparent[c]) {
738
match = false;
739
}
740
}
741
742
if (match) {
743
scratch.put(i+3, (byte) 0);
744
}
745
}
746
}
747
748
scratch.position(0);
749
750
return scratch;
751
}
752
753
/**
754
* Safe convert byte to int
755
*
756
* @param b The byte to convert
757
* @return The converted byte
758
*/
759
private int toInt(byte b) {
760
if (b < 0) {
761
return 256+b;
762
}
763
764
return b;
765
}
766
767
/**
768
* Get the closest greater power of 2 to the fold number
769
*
770
* @param fold The target number
771
* @return The power of 2
772
*/
773
private int get2Fold(int fold) {
774
int ret = 2;
775
while (ret < fold) {
776
ret *= 2;
777
}
778
return ret;
779
}
780
781
/**
782
* @see org.newdawn.slick.opengl.LoadableImageData#configureEdging(boolean)
783
*/
784
public void configureEdging(boolean edging) {
785
}
786
}
787
788
789