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 }