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 }