forked from adamdruppe/arsd
-
Notifications
You must be signed in to change notification settings - Fork 0
/
png.d
2141 lines (1747 loc) · 57.1 KB
/
png.d
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
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
/++
PNG file read and write. Leverages [arsd.color|color.d]'s [MemoryImage] interfaces for interop.
The main high-level functions you want are [readPng], [readPngFromBytes], [writePng], and maybe [writeImageToPngFile] or [writePngLazy] for some circumstances.
The other functions are low-level implementations and helpers for dissecting the png file format.
History:
Originally written in 2009. This is why some of it is still written in a C-like style!
See_Also:
$(LIST
* [arsd.image] has generic load interfaces that can handle multiple file formats, including png.
* [arsd.apng] handles the animated png extensions.
)
+/
module arsd.png;
import core.memory;
/++
Easily reads a png file into a [MemoryImage]
Returns:
Please note this function doesn't return null right now, but you should still check for null anyway as that might change.
The returned [MemoryImage] is either a [IndexedImage] or a [TrueColorImage], depending on the file's color mode. You can cast it to one or the other, or just call [MemoryImage.getAsTrueColorImage] which will cast and return or convert as needed automatically.
Greyscale pngs and bit depths other than 8 are converted for the ease of the MemoryImage interface. If you need more detail, try [PNG] and [getDatastream] etc.
+/
MemoryImage readPng(string filename) {
import std.file;
return imageFromPng(readPng(cast(ubyte[]) read(filename)));
}
/++
Easily reads a png from a data array into a MemoryImage.
History:
Added December 29, 2021 (dub v10.5)
+/
MemoryImage readPngFromBytes(const(ubyte)[] bytes) {
return imageFromPng(readPng(bytes));
}
/++
Saves a MemoryImage to a png file. See also: [writeImageToPngFile] which uses memory a little more efficiently
+/
void writePng(string filename, MemoryImage mi) {
// FIXME: it would be nice to write the file lazily so we don't have so many intermediate buffers here
PNG* png;
if(auto p = cast(IndexedImage) mi)
png = pngFromImage(p);
else if(auto p = cast(TrueColorImage) mi)
png = pngFromImage(p);
else assert(0);
import std.file;
std.file.write(filename, writePng(png));
}
/++
Represents the different types of png files, with numbers matching what the spec gives for filevalues.
+/
enum PngType {
greyscale = 0, /// The data must be `depth` bits per pixel
truecolor = 2, /// The data will be RGB triples, so `depth * 3` bits per pixel. Depth must be 8 or 16.
indexed = 3, /// The data must be `depth` bits per pixel, with a palette attached. Use [writePng] with [IndexedImage] for this mode. Depth must be <= 8.
greyscale_with_alpha = 4, /// The data must be (grey, alpha) byte pairs for each pixel. Thus `depth * 2` bits per pixel. Depth must be 8 or 16.
truecolor_with_alpha = 6 /// The data must be RGBA quads for each pixel. Thus, `depth * 4` bits per pixel. Depth must be 8 or 16.
}
/++
Saves an image from an existing array of pixel data. Note that depth other than 8 may not be implemented yet. Also note depth of 16 must be stored big endian
+/
void writePng(string filename, const ubyte[] data, int width, int height, PngType type, ubyte depth = 8) {
PngHeader h;
h.width = width;
h.height = height;
h.type = cast(ubyte) type;
h.depth = depth;
auto png = blankPNG(h);
addImageDatastreamToPng(data, png);
import std.file;
std.file.write(filename, writePng(png));
}
/*
//Here's a simple test program that shows how to write a quick image viewer with simpledisplay:
import arsd.png;
import arsd.simpledisplay;
import std.file;
void main(string[] args) {
// older api, the individual functions give you more control if you need it
//auto img = imageFromPng(readPng(cast(ubyte[]) read(args[1])));
// newer api, simpler but less control
auto img = readPng(args[1]);
// displayImage is from simpledisplay and just pops up a window to show the image
// simpledisplay's Images are a little different than MemoryImages that this loads,
// but conversion is easy
displayImage(Image.fromMemoryImage(img));
}
*/
// By Adam D. Ruppe, 2009-2010, released into the public domain
//import std.file;
//import std.zlib;
public import arsd.color;
/**
The return value should be casted to indexed or truecolor depending on what the file is. You can
also use getAsTrueColorImage to forcibly convert it if needed.
To get an image from a png file, do something like this:
auto i = cast(TrueColorImage) imageFromPng(readPng(cast(ubyte)[]) std.file.read("file.png")));
*/
MemoryImage imageFromPng(PNG* png) {
PngHeader h = getHeader(png);
/** Types from the PNG spec:
0 - greyscale
2 - truecolor
3 - indexed color
4 - grey with alpha
6 - true with alpha
1, 5, and 7 are invalid.
There's a kind of bitmask going on here:
If type&1, it has a palette.
If type&2, it is in color.
If type&4, it has an alpha channel in the datastream.
*/
MemoryImage i;
ubyte[] idata;
// FIXME: some duplication with the lazy reader below in the module
switch(h.type) {
case 0: // greyscale
case 4: // greyscale with alpha
// this might be a different class eventually...
auto a = new TrueColorImage(h.width, h.height);
idata = a.imageData.bytes;
i = a;
break;
case 2: // truecolor
case 6: // truecolor with alpha
auto a = new TrueColorImage(h.width, h.height);
idata = a.imageData.bytes;
i = a;
break;
case 3: // indexed
auto a = new IndexedImage(h.width, h.height);
a.palette = fetchPalette(png);
a.hasAlpha = true; // FIXME: don't be so conservative here
idata = a.data;
i = a;
break;
default:
assert(0, "invalid png");
}
size_t idataIdx = 0;
auto file = LazyPngFile!(Chunk[])(png.chunks);
immutable(ubyte)[] previousLine;
auto bpp = bytesPerPixel(h);
foreach(line; file.rawDatastreamByChunk()) {
auto filter = line[0];
auto data = unfilter(filter, line[1 .. $], previousLine, bpp);
previousLine = data;
convertPngData(h.type, h.depth, data, h.width, idata, idataIdx);
}
assert(idataIdx == idata.length, "not all filled, wtf");
assert(i !is null);
return i;
}
/+
This is used by the load MemoryImage functions to convert the png'd datastream into the format MemoryImage's implementations expect.
idata needs to be already sized for the image! width * height if indexed, width*height*4 if not.
+/
void convertPngData(ubyte type, ubyte depth, const(ubyte)[] data, int width, ubyte[] idata, ref size_t idataIdx) {
ubyte consumeOne() {
ubyte ret = data[0];
data = data[1 .. $];
return ret;
}
import std.conv;
loop: for(int pixel = 0; pixel < width; pixel++)
switch(type) {
case 0: // greyscale
case 4: // greyscale with alpha
case 3: // indexed
void acceptPixel(ubyte p) {
if(type == 3) {
idata[idataIdx++] = p;
} else {
if(depth == 1) {
p = p ? 0xff : 0;
} else if (depth == 2) {
p |= p << 2;
p |= p << 4;
}
else if (depth == 4) {
p |= p << 4;
}
idata[idataIdx++] = p;
idata[idataIdx++] = p;
idata[idataIdx++] = p;
if(type == 0)
idata[idataIdx++] = 255;
else if(type == 4)
idata[idataIdx++] = consumeOne();
}
}
auto b = consumeOne();
switch(depth) {
case 1:
acceptPixel((b >> 7) & 0x01);
pixel++; if(pixel == width) break loop;
acceptPixel((b >> 6) & 0x01);
pixel++; if(pixel == width) break loop;
acceptPixel((b >> 5) & 0x01);
pixel++; if(pixel == width) break loop;
acceptPixel((b >> 4) & 0x01);
pixel++; if(pixel == width) break loop;
acceptPixel((b >> 3) & 0x01);
pixel++; if(pixel == width) break loop;
acceptPixel((b >> 2) & 0x01);
pixel++; if(pixel == width) break loop;
acceptPixel((b >> 1) & 0x01);
pixel++; if(pixel == width) break loop;
acceptPixel(b & 0x01);
break;
case 2:
acceptPixel((b >> 6) & 0x03);
pixel++; if(pixel == width) break loop;
acceptPixel((b >> 4) & 0x03);
pixel++; if(pixel == width) break loop;
acceptPixel((b >> 2) & 0x03);
pixel++; if(pixel == width) break loop;
acceptPixel(b & 0x03);
break;
case 4:
acceptPixel((b >> 4) & 0x0f);
pixel++; if(pixel == width) break loop;
acceptPixel(b & 0x0f);
break;
case 8:
acceptPixel(b);
break;
case 16:
assert(type != 3); // 16 bit indexed isn't supported per png spec
acceptPixel(b);
consumeOne(); // discarding the least significant byte as we can't store it anyway
break;
default:
assert(0, "bit depth not implemented");
}
break;
case 2: // truecolor
case 6: // true with alpha
if(depth == 8) {
idata[idataIdx++] = consumeOne();
idata[idataIdx++] = consumeOne();
idata[idataIdx++] = consumeOne();
idata[idataIdx++] = (type == 6) ? consumeOne() : 255;
} else if(depth == 16) {
idata[idataIdx++] = consumeOne();
consumeOne();
idata[idataIdx++] = consumeOne();
consumeOne();
idata[idataIdx++] = consumeOne();
consumeOne();
idata[idataIdx++] = (type == 6) ? consumeOne() : 255;
if(type == 6)
consumeOne();
} else assert(0, "unsupported truecolor bit depth " ~ to!string(depth));
break;
default: assert(0);
}
assert(data.length == 0, "not all consumed, wtf " ~ to!string(data));
}
/*
struct PngHeader {
uint width;
uint height;
ubyte depth = 8;
ubyte type = 6; // 0 - greyscale, 2 - truecolor, 3 - indexed color, 4 - grey with alpha, 6 - true with alpha
ubyte compressionMethod = 0; // should be zero
ubyte filterMethod = 0; // should be zero
ubyte interlaceMethod = 0; // bool
}
*/
/++
Creates the [PNG] data structure out of an [IndexedImage]. This structure will have the minimum number of colors
needed to represent the image faithfully in the file and will be ready for writing to a file.
This is called by [writePng].
+/
PNG* pngFromImage(IndexedImage i) {
PngHeader h;
h.width = i.width;
h.height = i.height;
h.type = 3;
if(i.numColors() <= 2)
h.depth = 1;
else if(i.numColors() <= 4)
h.depth = 2;
else if(i.numColors() <= 16)
h.depth = 4;
else if(i.numColors() <= 256)
h.depth = 8;
else throw new Exception("can't save this as an indexed png");
auto png = blankPNG(h);
// do palette and alpha
// FIXME: if there is only one transparent color, set it as the special chunk for that
// FIXME: we'd get a smaller file size if the transparent pixels were arranged first
Chunk palette;
palette.type = ['P', 'L', 'T', 'E'];
palette.size = cast(int) i.palette.length * 3;
palette.payload.length = palette.size;
Chunk alpha;
if(i.hasAlpha) {
alpha.type = ['t', 'R', 'N', 'S'];
alpha.size = cast(uint) i.palette.length;
alpha.payload.length = alpha.size;
}
for(int a = 0; a < i.palette.length; a++) {
palette.payload[a*3+0] = i.palette[a].r;
palette.payload[a*3+1] = i.palette[a].g;
palette.payload[a*3+2] = i.palette[a].b;
if(i.hasAlpha)
alpha.payload[a] = i.palette[a].a;
}
palette.checksum = crc("PLTE", palette.payload);
png.chunks ~= palette;
if(i.hasAlpha) {
alpha.checksum = crc("tRNS", alpha.payload);
png.chunks ~= alpha;
}
// do the datastream
if(h.depth == 8) {
addImageDatastreamToPng(i.data, png);
} else {
// gotta convert it
auto bitsPerLine = i.width * h.depth;
if(bitsPerLine % 8 != 0)
bitsPerLine = bitsPerLine / 8 + 1;
else
bitsPerLine = bitsPerLine / 8;
ubyte[] datastream = new ubyte[bitsPerLine * i.height];
int shift = 0;
switch(h.depth) {
default: assert(0);
case 1: shift = 7; break;
case 2: shift = 6; break;
case 4: shift = 4; break;
case 8: shift = 0; break;
}
size_t dsp = 0;
size_t dpos = 0;
bool justAdvanced;
for(int y = 0; y < i.height; y++) {
for(int x = 0; x < i.width; x++) {
datastream[dsp] |= i.data[dpos++] << shift;
switch(h.depth) {
default: assert(0);
case 1: shift-= 1; break;
case 2: shift-= 2; break;
case 4: shift-= 4; break;
case 8: shift-= 8; break;
}
justAdvanced = shift < 0;
if(shift < 0) {
dsp++;
switch(h.depth) {
default: assert(0);
case 1: shift = 7; break;
case 2: shift = 6; break;
case 4: shift = 4; break;
case 8: shift = 0; break;
}
}
}
if(!justAdvanced)
dsp++;
switch(h.depth) {
default: assert(0);
case 1: shift = 7; break;
case 2: shift = 6; break;
case 4: shift = 4; break;
case 8: shift = 0; break;
}
}
addImageDatastreamToPng(datastream, png);
}
return png;
}
/++
Creates the [PNG] data structure out of a [TrueColorImage]. This implementation currently always make
the file a true color with alpha png type.
This is called by [writePng].
+/
PNG* pngFromImage(TrueColorImage i) {
PngHeader h;
h.width = i.width;
h.height = i.height;
// FIXME: optimize it if it is greyscale or doesn't use alpha alpha
auto png = blankPNG(h);
addImageDatastreamToPng(i.imageData.bytes, png);
return png;
}
/*
void main(string[] args) {
auto a = readPng(cast(ubyte[]) read(args[1]));
auto f = getDatastream(a);
foreach(i; f) {
writef("%d ", i);
}
writefln("\n\n%d", f.length);
}
*/
/++
Represents the PNG file's data. This struct is intended to be passed around by pointer.
+/
struct PNG {
/++
The length of the file.
+/
uint length;
/++
The PNG file magic number header. Please note the image data header is a IHDR chunk, not this (see [getHeader] for that). This just a static identifier
History:
Prior to October 10, 2022, this was called `header`.
+/
ubyte[8] magic;// = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; // this is the only valid value but idk if it is worth changing here since the ctor sets it regardless.
/// ditto
deprecated("use `magic` instead") alias header = magic;
/++
The array of chunks that make up the file contents. See [getChunkNullable], [getChunk], [insertChunk], and [replaceChunk] for functions to access and manipulate this array.
+/
Chunk[] chunks;
/++
Gets the chunk with the given name, or throws if it cannot be found.
Returns:
A non-null pointer to the chunk in the [chunks] array.
Throws:
an exception if the chunk can not be found. The type of this exception is subject to change at this time.
See_Also:
[getChunkNullable], which returns a null pointer instead of throwing.
+/
pure @trusted /* see note on getChunkNullable */
Chunk* getChunk(string what) {
foreach(ref c; chunks) {
if(c.stype == what)
return &c;
}
throw new Exception("no such chunk " ~ what);
}
/++
Gets the chunk with the given name, return `null` if it is not found.
See_Also:
[getChunk], which throws if the chunk cannot be found.
+/
nothrow @nogc pure @trusted /* trusted because &c i know is referring to the dynamic array, not actually a local. That has lifetime at least as much of the parent PNG object. */
Chunk* getChunkNullable(string what) {
foreach(ref c; chunks) {
if(c.stype == what)
return &c;
}
return null;
}
/++
Insert chunk before IDAT. PNG specs allows to drop all chunks after IDAT,
so we have to insert our custom chunks right before it.
Use `Chunk.create()` to create new chunk, and then `insertChunk()` to add it.
Return `true` if we did replacement.
+/
nothrow pure @trusted /* the chunks.ptr here fails safe, but it does that for performance and again I control that data so can be reasonably assured */
bool insertChunk (Chunk* chk, bool replaceExisting=false) {
if (chk is null) return false; // just in case
// use reversed loop, as "IDAT" is usually present, and it is usually the last,
// so we will somewhat amortize painter's algorithm here.
foreach_reverse (immutable idx, ref cc; chunks) {
if (replaceExisting && cc.type == chk.type) {
// replace existing chunk, the easiest case
chunks[idx] = *chk;
return true;
}
if (cc.stype == "IDAT") {
// ok, insert it; and don't use phobos
chunks.length += 1;
foreach_reverse (immutable c; idx+1..chunks.length) chunks.ptr[c] = chunks.ptr[c-1];
chunks.ptr[idx] = *chk;
return false;
}
}
chunks ~= *chk;
return false;
}
/++
Convenient wrapper for `insertChunk()`.
+/
nothrow pure @safe
bool replaceChunk (Chunk* chk) { return insertChunk(chk, true); }
}
/++
this is just like writePng(filename, pngFromImage(image)), but it manages
its own memory and writes straight to the file instead of using intermediate buffers that might not get gc'd right
+/
void writeImageToPngFile(in char[] filename, TrueColorImage image) {
PNG* png;
ubyte[] com;
{
import std.zlib;
PngHeader h;
h.width = image.width;
h.height = image.height;
png = blankPNG(h);
size_t bytesPerLine = cast(size_t)h.width * 4;
if(h.type == 3)
bytesPerLine = cast(size_t)h.width * 8 / h.depth;
Chunk dat;
dat.type = ['I', 'D', 'A', 'T'];
size_t pos = 0;
auto compressor = new Compress();
import core.stdc.stdlib;
auto lineBuffer = (cast(ubyte*)malloc(1 + bytesPerLine))[0 .. 1+bytesPerLine];
scope(exit) free(lineBuffer.ptr);
while(pos+bytesPerLine <= image.imageData.bytes.length) {
lineBuffer[0] = 0;
lineBuffer[1..1+bytesPerLine] = image.imageData.bytes[pos.. pos+bytesPerLine];
com ~= cast(ubyte[]) compressor.compress(lineBuffer);
pos += bytesPerLine;
}
com ~= cast(ubyte[]) compressor.flush();
assert(com.length <= uint.max);
dat.size = cast(uint) com.length;
dat.payload = com;
dat.checksum = crc("IDAT", dat.payload);
png.chunks ~= dat;
Chunk c;
c.size = 0;
c.type = ['I', 'E', 'N', 'D'];
c.checksum = crc("IEND", c.payload);
png.chunks ~= c;
}
assert(png !is null);
import core.stdc.stdio;
import std.string;
FILE* fp = fopen(toStringz(filename), "wb");
if(fp is null)
throw new Exception("Couldn't open png file for writing.");
scope(exit) fclose(fp);
fwrite(png.magic.ptr, 1, 8, fp);
foreach(c; png.chunks) {
fputc((c.size & 0xff000000) >> 24, fp);
fputc((c.size & 0x00ff0000) >> 16, fp);
fputc((c.size & 0x0000ff00) >> 8, fp);
fputc((c.size & 0x000000ff) >> 0, fp);
fwrite(c.type.ptr, 1, 4, fp);
fwrite(c.payload.ptr, 1, c.size, fp);
fputc((c.checksum & 0xff000000) >> 24, fp);
fputc((c.checksum & 0x00ff0000) >> 16, fp);
fputc((c.checksum & 0x0000ff00) >> 8, fp);
fputc((c.checksum & 0x000000ff) >> 0, fp);
}
{ import core.memory : GC; GC.free(com.ptr); } // there is a reference to this in the PNG struct, but it is going out of scope here too, so who cares
// just wanna make sure this crap doesn't stick around
}
/++
Turns a [PNG] structure into an array of bytes, ready to be written to a file.
+/
ubyte[] writePng(PNG* p) {
ubyte[] a;
if(p.length)
a.length = p.length;
else {
a.length = 8;
foreach(c; p.chunks)
a.length += c.size + 12;
}
size_t pos;
a[0..8] = p.magic[0..8];
pos = 8;
foreach(c; p.chunks) {
a[pos++] = (c.size & 0xff000000) >> 24;
a[pos++] = (c.size & 0x00ff0000) >> 16;
a[pos++] = (c.size & 0x0000ff00) >> 8;
a[pos++] = (c.size & 0x000000ff) >> 0;
a[pos..pos+4] = c.type[0..4];
pos += 4;
a[pos..pos+c.size] = c.payload[0..c.size];
pos += c.size;
a[pos++] = (c.checksum & 0xff000000) >> 24;
a[pos++] = (c.checksum & 0x00ff0000) >> 16;
a[pos++] = (c.checksum & 0x0000ff00) >> 8;
a[pos++] = (c.checksum & 0x000000ff) >> 0;
}
return a;
}
/++
Opens a file and pulls the [PngHeader] out, leaving the rest of the data alone.
This might be useful when you're only interested in getting a file's image size or
other basic metainfo without loading the whole thing.
+/
PngHeader getHeaderFromFile(string filename) {
import std.stdio;
auto file = File(filename, "rb");
ubyte[12] initialBuffer; // file header + size of first chunk (should be IHDR)
auto data = file.rawRead(initialBuffer[]);
if(data.length != 12)
throw new Exception("couldn't get png file header off " ~ filename);
if(data[0..8] != [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
throw new Exception("file " ~ filename ~ " is not a png");
size_t pos = 8;
size_t size;
size |= data[pos++] << 24;
size |= data[pos++] << 16;
size |= data[pos++] << 8;
size |= data[pos++] << 0;
size += 4; // chunk type
size += 4; // checksum
ubyte[] more;
more.length = size;
auto chunk = file.rawRead(more);
if(chunk.length != size)
throw new Exception("couldn't get png image header off " ~ filename);
more = data ~ chunk;
auto png = readPng(more);
return getHeader(png);
}
/++
Given an in-memory array of bytes from a PNG file, returns the parsed out [PNG] object.
You might want the other [readPng] overload instead, which returns an even more processed [MemoryImage] object.
+/
PNG* readPng(in ubyte[] data) {
auto p = new PNG;
p.length = cast(int) data.length;
p.magic[0..8] = data[0..8];
if(p.magic != [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a])
throw new Exception("not a png, header wrong");
size_t pos = 8;
while(pos < data.length) {
Chunk n;
n.size |= data[pos++] << 24;
n.size |= data[pos++] << 16;
n.size |= data[pos++] << 8;
n.size |= data[pos++] << 0;
n.type[0..4] = data[pos..pos+4];
pos += 4;
n.payload.length = n.size;
if(pos + n.size > data.length)
throw new Exception(format("malformed png, chunk '%s' %d @ %d longer than data %d", n.type, n.size, pos, data.length));
if(pos + n.size < pos)
throw new Exception("uint overflow: chunk too large");
n.payload[0..n.size] = data[pos..pos+n.size];
pos += n.size;
n.checksum |= data[pos++] << 24;
n.checksum |= data[pos++] << 16;
n.checksum |= data[pos++] << 8;
n.checksum |= data[pos++] << 0;
p.chunks ~= n;
}
return p;
}
/++
Creates a new [PNG] object from the given header parameters, ready to receive data.
+/
PNG* blankPNG(PngHeader h) {
auto p = new PNG;
p.magic = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
Chunk c;
c.size = 13;
c.type = ['I', 'H', 'D', 'R'];
c.payload.length = 13;
size_t pos = 0;
c.payload[pos++] = h.width >> 24;
c.payload[pos++] = (h.width >> 16) & 0xff;
c.payload[pos++] = (h.width >> 8) & 0xff;
c.payload[pos++] = h.width & 0xff;
c.payload[pos++] = h.height >> 24;
c.payload[pos++] = (h.height >> 16) & 0xff;
c.payload[pos++] = (h.height >> 8) & 0xff;
c.payload[pos++] = h.height & 0xff;
c.payload[pos++] = h.depth;
c.payload[pos++] = h.type;
c.payload[pos++] = h.compressionMethod;
c.payload[pos++] = h.filterMethod;
c.payload[pos++] = h.interlaceMethod;
c.checksum = crc("IHDR", c.payload);
p.chunks ~= c;
return p;
}
/+
Implementation helper for creating png files.
Its API is subject to change; it would be private except it might be useful to you.
+/
// should NOT have any idata already.
// FIXME: doesn't handle palettes
void addImageDatastreamToPng(const(ubyte)[] data, PNG* png, bool addIend = true) {
// we need to go through the lines and add the filter byte
// then compress it into an IDAT chunk
// then add the IEND chunk
import std.zlib;
PngHeader h = getHeader(png);
if(h.depth == 0)
throw new Exception("depth of zero makes no sense");
if(h.width == 0)
throw new Exception("width zero?!!?!?!");
int multiplier;
size_t bytesPerLine;
switch(h.type) {
case 0:
multiplier = 1;
break;
case 2:
multiplier = 3;
break;
case 3:
multiplier = 1;
break;
case 4:
multiplier = 2;
break;
case 6:
multiplier = 4;
break;
default: assert(0);
}
bytesPerLine = h.width * multiplier * h.depth / 8;
if((h.width * multiplier * h.depth) % 8 != 0)
bytesPerLine += 1;
assert(bytesPerLine >= 1);
Chunk dat;
dat.type = ['I', 'D', 'A', 'T'];
size_t pos = 0;
const(ubyte)[] output;
while(pos+bytesPerLine <= data.length) {
output ~= 0;
output ~= data[pos..pos+bytesPerLine];
pos += bytesPerLine;
}
auto com = cast(ubyte[]) compress(output);
dat.size = cast(int) com.length;
dat.payload = com;
dat.checksum = crc("IDAT", dat.payload);
png.chunks ~= dat;
if(addIend) {
Chunk c;
c.size = 0;
c.type = ['I', 'E', 'N', 'D'];
c.checksum = crc("IEND", c.payload);
png.chunks ~= c;
}
}
deprecated alias PngHeader PNGHeader;
// bKGD - palette entry for background or the RGB (16 bits each) for that. or 16 bits of grey
/+
Uncompresses the raw datastream out of the file chunks, but does not continue processing it, so the scanlines are still filtered, etc.
+/
ubyte[] getDatastream(PNG* p) {
import std.zlib;
ubyte[] compressed;
foreach(c; p.chunks) {
if(c.stype != "IDAT")
continue;
compressed ~= c.payload;
}
return cast(ubyte[]) uncompress(compressed);
}
/+
Gets a raw datastream out of a 8 bpp png. See also [getANDMask]
+/
// FIXME: Assuming 8 bits per pixel
ubyte[] getUnfilteredDatastream(PNG* p) {
PngHeader h = getHeader(p);
assert(h.filterMethod == 0);
assert(h.type == 3); // FIXME
assert(h.depth == 8); // FIXME
ubyte[] data = getDatastream(p);
ubyte[] ufdata = new ubyte[data.length - h.height];
int bytesPerLine = cast(int) ufdata.length / h.height;
int pos = 0, pos2 = 0;
for(int a = 0; a < h.height; a++) {
assert(data[pos2] == 0);
ufdata[pos..pos+bytesPerLine] = data[pos2+1..pos2+bytesPerLine+1];
pos+= bytesPerLine;
pos2+= bytesPerLine + 1;
}
return ufdata;
}
/+
Gets the unfiltered raw datastream for conversion to Windows ico files. See also [getANDMask] and [fetchPaletteWin32].
+/
ubyte[] getFlippedUnfilteredDatastream(PNG* p) {
PngHeader h = getHeader(p);
assert(h.filterMethod == 0);
assert(h.type == 3); // FIXME
assert(h.depth == 8 || h.depth == 4); // FIXME
ubyte[] data = getDatastream(p);
ubyte[] ufdata = new ubyte[data.length - h.height];
int bytesPerLine = cast(int) ufdata.length / h.height;
int pos = cast(int) ufdata.length - bytesPerLine, pos2 = 0;
for(int a = 0; a < h.height; a++) {
assert(data[pos2] == 0);
ufdata[pos..pos+bytesPerLine] = data[pos2+1..pos2+bytesPerLine+1];
pos-= bytesPerLine;
pos2+= bytesPerLine + 1;
}
return ufdata;
}
ubyte getHighNybble(ubyte a) {
return cast(ubyte)(a >> 4); // FIXME
}
ubyte getLowNybble(ubyte a) {
return a & 0x0f;
}
/++
Takes the transparency info and returns an AND mask suitable for use in a Windows ico
+/
ubyte[] getANDMask(PNG* p) {
PngHeader h = getHeader(p);
assert(h.filterMethod == 0);
assert(h.type == 3); // FIXME
assert(h.depth == 8 || h.depth == 4); // FIXME
assert(h.width % 8 == 0); // might actually be %2
ubyte[] data = getDatastream(p);
ubyte[] ufdata = new ubyte[h.height*((((h.width+7)/8)+3)&~3)]; // gotta pad to DWORDs...
Color[] colors = fetchPalette(p);
int pos = 0, pos2 = (h.width/((h.depth == 8) ? 1 : 2)+1)*(h.height-1);
bool bits = false;
for(int a = 0; a < h.height; a++) {
assert(data[pos2++] == 0);
for(int b = 0; b < h.width; b++) {
if(h.depth == 4) {
ufdata[pos/8] |= ((colors[bits? getLowNybble(data[pos2]) : getHighNybble(data[pos2])].a <= 30) << (7-(pos%8)));
} else
ufdata[pos/8] |= ((colors[data[pos2]].a == 0) << (7-(pos%8)));
pos++;
if(h.depth == 4) {
if(bits) {
pos2++;
}
bits = !bits;
} else
pos2++;
}
int pad = 0;
for(; pad < ((pos/8) % 4); pad++) {
ufdata[pos/8] = 0;
pos+=8;