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 }