1 /**
2 PNG support.
3 
4 Copyright: Copyright Guillaume Piolat 2022
5 License:   $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0)
6 */
7 module gamut.plugins.png;
8 
9 nothrow @nogc @safe:
10 
11 import core.stdc.stdlib: malloc, free, realloc;
12 import gamut.types;
13 import gamut.io;
14 import gamut.plugin;
15 import gamut.image;
16 import gamut.internals.errors;
17 import gamut.internals.types;
18 
19 version(decodePNG) import gamut.codecs.stbdec;
20 version(encodePNG) import gamut.codecs.stb_image_write;
21 
22 ImageFormatPlugin makePNGPlugin()
23 {
24     ImageFormatPlugin p;
25     p.format = "PNG";
26     p.extensionList = "png";
27     p.mimeTypes = "image/png";
28     version(decodePNG)
29         p.loadProc = &loadPNG;
30     else
31         p.loadProc = null;
32     version(encodePNG)
33         p.saveProc = &savePNG;
34     else
35         p.saveProc = null;
36     p.detectProc = &detectPNG;
37     return p;
38 }
39 
40 
41 // PERF: STB callbacks could disappear in favor of our own callbakcs, to avoid one step.
42 
43 version(decodePNG)
44 void loadPNG(ref Image image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
45 {
46     IOAndHandle ioh;
47     stbi_io_callbacks stb_callback;
48     initSTBCallbacks(io, handle, &ioh, &stb_callback);
49 
50     bool is16bit = stbi__png_is16(&stb_callback, &ioh);
51 
52     int requestedComp = computeRequestedImageComponents(flags);
53     if (requestedComp == 0) // error
54     {
55         image.error(kStrInvalidFlags);
56         return;
57     }
58     if (requestedComp == -1)
59         requestedComp = 0; // auto
60 
61     // rewind stream
62     if (!io.rewind(handle))
63     {
64         image.error(kStrImageDecodingIOFailure);
65         return;
66     }
67 
68     ubyte* decoded;
69     int width, height, components;
70 
71     float ppmX = -1;
72     float ppmY = -1;
73     float pixelRatio = -1;
74 
75     // PERF: this could be overriden to use internal 8-bit <-> 10-bit stb conversion
76 
77     bool decodeTo16bit = is16bit;
78     if (flags & LOAD_8BIT) decodeTo16bit = false;
79     if (flags & LOAD_16BIT) decodeTo16bit = true;
80 
81     if (decodeTo16bit)
82     {
83         decoded = cast(ubyte*) stbi_load_16_from_callbacks(&stb_callback, &ioh, &width, &height, &components, requestedComp,
84                                                            &ppmX, &ppmY, &pixelRatio);
85     }
86     else
87     {
88         decoded = stbi_load_from_callbacks(&stb_callback, &ioh, &width, &height, &components, requestedComp,
89                                            &ppmX, &ppmY, &pixelRatio);
90     }
91 
92     if (requestedComp != 0)
93         components = requestedComp;
94 
95     if (decoded is null)
96     {
97         image.error(kStrImageDecodingFailed);
98         return;
99     }
100 
101     if (!imageIsValidSize(1, width, height))
102     {
103         image.error(kStrImageTooLarge);
104         free(decoded);
105         return;
106     }
107 
108     image._allocArea = decoded; // works because codec.pngload and gamut both use malloc/free
109     image._width = width;
110     image._height = height;
111     image._data = decoded; 
112     image._pitch = width * components * (decodeTo16bit ? 2 : 1);
113 
114     image._pixelAspectRatio = (pixelRatio == -1) ? GAMUT_UNKNOWN_ASPECT_RATIO : pixelRatio;
115     image._resolutionY = (ppmY == -1) ? GAMUT_UNKNOWN_RESOLUTION : convertInchesToMeters(ppmY);
116     image._layoutConstraints = LAYOUT_DEFAULT; // STB decoder follows no particular constraints (TODO?)
117     image._layerCount = 1;
118     image._layerOffset = 0;
119 
120     if (!decodeTo16bit)
121     {
122         if (components == 1)
123         {
124             image._type = PixelType.l8;
125         }
126         else if (components == 2)
127         {
128             image._type = PixelType.la8;
129         }
130         else if (components == 3)
131         {
132             image._type = PixelType.rgb8;
133         }
134         else if (components == 4)
135         {
136             image._type = PixelType.rgba8;
137         }
138     }
139     else
140     {
141         if (components == 1)
142         {
143             image._type = PixelType.l16;
144         }
145         else if (components == 2)
146         {
147             image._type = PixelType.la16;
148         }
149         else if (components == 3)
150         {
151             image._type = PixelType.rgb16;
152         }
153         else if (components == 4)
154         {
155             image._type = PixelType.rgba16;
156         }
157     }
158 
159     PixelType targetType = applyLoadFlags(image._type, flags);
160 
161     // Convert to target type and constraints
162     image.convertTo(targetType, cast(LayoutConstraints) flags);
163 }
164 
165 bool detectPNG(IOStream *io, IOHandle handle) @trusted
166 {
167     static immutable ubyte[8] pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
168     return fileIsStartingWithSignature(io, handle, pngSignature);
169 }
170 
171 version(encodePNG)
172 bool savePNG(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
173 {
174     if (page != 0)
175         return false;
176 
177     int channels = 0;
178     bool is16Bit = false;
179     switch (image._type)
180     {
181         case PixelType.l8:     channels = 1; break;
182         case PixelType.la8:    channels = 2; break;
183         case PixelType.rgb8:   channels = 3; break;
184         case PixelType.rgba8:  channels = 4; break;
185         case PixelType.l16:    channels = 1; is16Bit = true; break;
186         case PixelType.la16:   channels = 2; is16Bit = true; break;
187         case PixelType.rgb16:  channels = 3; is16Bit = true; break;
188         case PixelType.rgba16: channels = 4; is16Bit = true; break;
189         default:
190             return false;
191     }
192 
193     int width = image._width;
194     int height = image._height;
195     int pitch = image._pitch;
196 
197     int len;
198     const(ubyte)* pixels = image._data;
199 
200     // PERF: use stb_image_write stbi_write_png_to_func instead.
201     ubyte *encoded = gamut.codecs.stb_image_write.stbi_write_png_to_mem(pixels, pitch, width, height, channels, &len, is16Bit);
202     if (encoded == null)
203         return false;
204 
205     scope(exit) free(encoded);
206 
207     // Write all output at once. This is rather bad, could be done progressively.
208     // PERF: adapt stb_image_write.h to output in our own buffer directly.
209     if (len != io.write(encoded, 1, len, handle))
210         return false;
211 
212     return true;
213 }