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.pngload;
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     ioh.io = io;
48     ioh.handle = handle;
49 
50     stbi_io_callbacks stb_callback;
51     stb_callback.read = &stb_read;
52     stb_callback.skip = &stb_skip;
53     stb_callback.eof = &stb_eof;
54 
55     bool is16bit = stbi__png_is16(&stb_callback, &ioh);
56 
57     ubyte* decoded;
58     int width, height, components;
59 
60     int requestedComp = computeRequestedImageComponents(flags);
61     if (requestedComp == 0) // error
62     {
63         image.error(kStrInvalidFlags);
64         return;
65     }
66     if (requestedComp == -1)
67         requestedComp = 0; // auto
68 
69     // rewind stream
70     if (!io.rewind(handle))
71     {
72         image.error(kStrImageDecodingIOFailure);
73         return;
74     }
75 
76     float ppmX = -1;
77     float ppmY = -1;
78     float pixelRatio = -1;
79 
80     // PERF: this could be overriden to use internal 8-bit <-> 10-bit stb conversion
81 
82     bool decodeTo16bit = is16bit;
83     if (flags & LOAD_8BIT) decodeTo16bit = false;
84     if (flags & LOAD_16BIT) decodeTo16bit = true;
85 
86     if (decodeTo16bit)
87     {
88         decoded = cast(ubyte*) stbi_load_16_from_callbacks(&stb_callback, &ioh, &width, &height, &components, requestedComp,
89                                                            &ppmX, &ppmY, &pixelRatio);
90     }
91     else
92     {
93         decoded = stbi_load_from_callbacks(&stb_callback, &ioh, &width, &height, &components, requestedComp,
94                                            &ppmX, &ppmY, &pixelRatio);
95     }
96 
97     if (requestedComp != 0)
98         components = requestedComp;
99 
100     if (decoded is null)
101     {
102         image.error(kStrImageDecodingFailed);
103         return;
104     }    
105 
106     if (!imageIsValidSize(width, height))
107     {
108         image.error(kStrImageTooLarge);
109         free(decoded);
110         return;
111     }
112 
113     image._allocArea = decoded; // works because codec.pngload and gamut both use malloc/free
114     image._width = width;
115     image._height = height;
116     image._data = decoded; 
117     image._pitch = width * components * (decodeTo16bit ? 2 : 1);
118 
119     image._pixelAspectRatio = (pixelRatio == -1) ? GAMUT_UNKNOWN_ASPECT_RATIO : pixelRatio;
120     image._resolutionY = (ppmY == -1) ? GAMUT_UNKNOWN_RESOLUTION : convertInchesToMeters(ppmY);
121     image._layoutConstraints = LAYOUT_DEFAULT; // STB decoder follows no particular constraints (TODO?)
122 
123     if (!decodeTo16bit)
124     {
125         if (components == 1)
126         {
127             image._type = PixelType.l8;
128         }
129         else if (components == 2)
130         {
131             image._type = PixelType.la8;
132         }
133         else if (components == 3)
134         {
135             image._type = PixelType.rgb8;
136         }
137         else if (components == 4)
138         {
139             image._type = PixelType.rgba8;
140         }
141     }
142     else
143     {
144         if (components == 1)
145         {
146             image._type = PixelType.l16;
147         }
148         else if (components == 2)
149         {
150             image._type = PixelType.la16;
151         }
152         else if (components == 3)
153         {
154             image._type = PixelType.rgb16;
155         }
156         else if (components == 4)
157         {
158             image._type = PixelType.rgba16;
159         }
160     }
161 
162     PixelType targetType = applyLoadFlags(image._type, flags);
163 
164     // Convert to target type and constraints
165     image.convertTo(targetType, cast(LayoutConstraints) flags);
166 }
167 
168 bool detectPNG(IOStream *io, IOHandle handle) @trusted
169 {
170     static immutable ubyte[8] pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
171     return fileIsStartingWithSignature(io, handle, pngSignature);
172 }
173 
174 version(encodePNG)
175 bool savePNG(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
176 {
177     if (page != 0)
178         return false;
179 
180     int channels = 0;
181     switch (image._type)
182     {
183         case PixelType.l8:     channels = 1; break;
184         case PixelType.la8:    channels = 2; break;
185         case PixelType.rgb8:   channels = 3; break;
186         case PixelType.rgba8:  channels = 4; break;
187         default:
188             return false;
189     }
190 
191     int width = image._width;
192     int height = image._height;
193     int pitch = image._pitch;
194     int len;
195     const(ubyte)* pixels = image._data;
196 
197     // PERF: use stb_image_write stbi_write_png_to_func instead.
198     ubyte *encoded = gamut.codecs.stb_image_write.stbi_write_png_to_mem(pixels, pitch, width, height, channels, &len);
199     if (encoded == null)
200         return false;
201 
202     scope(exit) free(encoded);
203 
204     // Write all output at once. This is rather bad, could be done progressively.
205     // PERF: adapt stb_image_write.h to output in our own buffer directly.
206     if (len != io.write(encoded, 1, len, handle))
207         return false;
208 
209     return true;
210 }
211 
212 private:
213 
214 // Need to give both a IOStream* and a IOHandle to STB callbacks.
215 static struct IOAndHandle
216 {
217     IOStream* io;
218     IOHandle handle;
219 }
220 
221 // fill 'data' with 'size' bytes.  return number of bytes actually read
222 int stb_read(void *user, char *data, int size) @system
223 {
224     IOAndHandle* ioh = cast(IOAndHandle*) user;
225 
226     // Cannot ask more than 0x7fff_ffff bytes at once.
227     assert(size <= 0x7fffffff);
228 
229     size_t bytesRead = ioh.io.read(data, 1, size, ioh.handle);
230     return cast(int) bytesRead;
231 }
232 
233 // skip the next 'n' bytes, or 'unget' the last -n bytes if negative
234 void stb_skip(void *user, int n) @system
235 {
236     IOAndHandle* ioh = cast(IOAndHandle*) user;
237     ioh.io.skipBytes(ioh.handle, n);
238 }
239 
240 // returns nonzero if we are at end of file/data
241 int stb_eof(void *user) @system
242 {
243     IOAndHandle* ioh = cast(IOAndHandle*) user;
244     return ioh.io.eof(ioh.handle);
245 }