The OpenD Programming Language

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     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(1, 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     image._layerCount = 1;
123     image._layerOffset = 0;
124 
125     if (!decodeTo16bit)
126     {
127         if (components == 1)
128         {
129             image._type = PixelType.l8;
130         }
131         else if (components == 2)
132         {
133             image._type = PixelType.la8;
134         }
135         else if (components == 3)
136         {
137             image._type = PixelType.rgb8;
138         }
139         else if (components == 4)
140         {
141             image._type = PixelType.rgba8;
142         }
143     }
144     else
145     {
146         if (components == 1)
147         {
148             image._type = PixelType.l16;
149         }
150         else if (components == 2)
151         {
152             image._type = PixelType.la16;
153         }
154         else if (components == 3)
155         {
156             image._type = PixelType.rgb16;
157         }
158         else if (components == 4)
159         {
160             image._type = PixelType.rgba16;
161         }
162     }
163 
164     PixelType targetType = applyLoadFlags(image._type, flags);
165 
166     // Convert to target type and constraints
167     image.convertTo(targetType, cast(LayoutConstraints) flags);
168 }
169 
170 bool detectPNG(IOStream *io, IOHandle handle) @trusted
171 {
172     static immutable ubyte[8] pngSignature = [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a];
173     return fileIsStartingWithSignature(io, handle, pngSignature);
174 }
175 
176 version(encodePNG)
177 bool savePNG(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
178 {
179     if (page != 0)
180         return false;
181 
182     int channels = 0;
183     bool is16Bit = false;
184     switch (image._type)
185     {
186         case PixelType.l8:     channels = 1; break;
187         case PixelType.la8:    channels = 2; break;
188         case PixelType.rgb8:   channels = 3; break;
189         case PixelType.rgba8:  channels = 4; break;
190         case PixelType.l16:    channels = 1; is16Bit = true; break;
191         case PixelType.la16:   channels = 2; is16Bit = true; break;
192         case PixelType.rgb16:  channels = 3; is16Bit = true; break;
193         case PixelType.rgba16: channels = 4; is16Bit = true; break;
194         default:
195             return false;
196     }
197 
198     int width = image._width;
199     int height = image._height;
200     int pitch = image._pitch;
201 
202     int len;
203     const(ubyte)* pixels = image._data;
204 
205     // PERF: use stb_image_write stbi_write_png_to_func instead.
206     ubyte *encoded = gamut.codecs.stb_image_write.stbi_write_png_to_mem(pixels, pitch, width, height, channels, &len, is16Bit);
207     if (encoded == null)
208         return false;
209 
210     scope(exit) free(encoded);
211 
212     // Write all output at once. This is rather bad, could be done progressively.
213     // PERF: adapt stb_image_write.h to output in our own buffer directly.
214     if (len != io.write(encoded, 1, len, handle))
215         return false;
216 
217     return true;
218 }
219 
220 private:
221 
222 // Need to give both a IOStream* and a IOHandle to STB callbacks.
223 static struct IOAndHandle
224 {
225     IOStream* io;
226     IOHandle handle;
227 }
228 
229 // fill 'data' with 'size' bytes.  return number of bytes actually read
230 int stb_read(void *user, char *data, int size) @system
231 {
232     IOAndHandle* ioh = cast(IOAndHandle*) user;
233 
234     // Cannot ask more than 0x7fff_ffff bytes at once.
235     assert(size <= 0x7fffffff);
236 
237     size_t bytesRead = ioh.io.read(data, 1, size, ioh.handle);
238     return cast(int) bytesRead;
239 }
240 
241 // skip the next 'n' bytes, or 'unget' the last -n bytes if negative
242 void stb_skip(void *user, int n) @system
243 {
244     IOAndHandle* ioh = cast(IOAndHandle*) user;
245     ioh.io.skipBytes(ioh.handle, n);
246 }
247 
248 // returns nonzero if we are at end of file/data
249 int stb_eof(void *user) @system
250 {
251     IOAndHandle* ioh = cast(IOAndHandle*) user;
252     return ioh.io.eof(ioh.handle);
253 }