1 /** 2 JPEG 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.jpeg; 8 9 nothrow @nogc @safe: 10 11 import core.stdc.stdlib: malloc, free, realloc; 12 import gamut.types; 13 import gamut.image; 14 import gamut.io; 15 import gamut.plugin; 16 import gamut.codecs.jpegload; 17 import gamut.codecs.stb_image_write; 18 import gamut.internals.errors; 19 import gamut.internals.types; 20 21 22 ImageFormatPlugin makeJPEGPlugin() 23 { 24 ImageFormatPlugin p; 25 p.format = "JPEG"; 26 p.extensionList = "jpg,jpeg,jif,jfif"; 27 p.mimeTypes = "image/jpeg"; 28 version(decodeJPEG) 29 p.loadProc = &loadJPEG; 30 else 31 p.loadProc = null; 32 version(encodeJPEG) 33 p.saveProc = &saveJPEG; 34 else 35 p.saveProc = null; 36 p.detectProc = &detectJPEG; 37 return p; 38 } 39 40 41 version(decodeJPEG) 42 void loadJPEG(ref Image image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted 43 { 44 JPEGIOHandle jio; 45 jio.wrapped = io; 46 jio.handle = handle; 47 48 int requestedComp = computeRequestedImageComponents(flags); 49 if (requestedComp == 0) 50 { 51 image.error(kStrInvalidFlags); 52 return; 53 } 54 55 if (requestedComp == 2) // JPEG reader doesn't convert to greyscale+alpha on the fly 56 requestedComp = -1; 57 58 int width, height, actualComp; 59 float pixelAspectRatio; 60 float dotsPerInchY; 61 ubyte[] decoded = decompress_jpeg_image_from_stream(&stream_read_jpeg, &jio, width, height, actualComp, pixelAspectRatio, dotsPerInchY, requestedComp); 62 if (decoded is null) 63 { 64 image.error(kStrImageDecodingFailed); 65 return; 66 } 67 68 if (actualComp != 1 && actualComp != 3 && actualComp != 4) 69 { 70 image.error(kStrImageWrongComponents); 71 free(decoded.ptr); 72 return; 73 } 74 75 if (!imageIsValidSize(1, width, height)) 76 { 77 image.error(kStrImageTooLarge); 78 free(decoded.ptr); 79 return; 80 } 81 82 int decodedComp = (requestedComp == -1) ? actualComp : requestedComp; 83 switch (decodedComp) 84 { 85 case 1: image._type = PixelType.l8; break; 86 case 3: image._type = PixelType.rgb8; break; 87 case 4: image._type = PixelType.rgba8; break; 88 default: 89 } 90 91 image._width = width; 92 image._height = height; 93 image._allocArea = decoded.ptr; 94 image._data = decoded.ptr; 95 image._pitch = width * decodedComp; 96 image._pixelAspectRatio = pixelAspectRatio == -1 ? GAMUT_UNKNOWN_ASPECT_RATIO : pixelAspectRatio; 97 image._resolutionY = dotsPerInchY == -1 ? GAMUT_UNKNOWN_RESOLUTION : dotsPerInchY; 98 image._layoutConstraints = LAYOUT_DEFAULT; // JPEG decoder follow no particular constraints (TODO?) 99 image._layerCount = 1; 100 image._layerOffset = 0; 101 102 // Convert to target type and constraints 103 image.convertTo(applyLoadFlags(image._type, flags), cast(LayoutConstraints) flags); 104 } 105 106 bool detectJPEG(IOStream *io, IOHandle handle) @trusted 107 { 108 static immutable ubyte[2] jpegSignature = [0xFF, 0xD8]; 109 return fileIsStartingWithSignature(io, handle, jpegSignature); 110 } 111 112 version(encodeJPEG) 113 bool saveJPEG(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted 114 { 115 if (page != 0) 116 return false; 117 118 int components; 119 120 switch (image._type) 121 { 122 case PixelType.l8: 123 components = 1; break; 124 case PixelType.rgb8: 125 components = 3; 126 break; 127 case PixelType.rgba8: 128 return false; // stb would throw away alpha 129 default: 130 return false; 131 } 132 133 JPEGIOHandle jio; 134 jio.wrapped = io; 135 jio.handle = handle; 136 137 void* userPointer = cast(void*)&jio; 138 139 int quality = 90; // TODO: option to choose that. 140 141 int res = stbi_write_jpg_to_func(&stb_stream_write, userPointer, 142 image._width, 143 image._height, 144 components, 145 image._data, quality); 146 147 return res == 1 && !jio.errored; 148 } 149 150 private: 151 152 153 struct JPEGIOHandle 154 { 155 IOStream* wrapped; 156 IOHandle handle; 157 158 // stb_image_write doesn't check errors for write, so keep a flag and start ignoring output if 159 // an I/O error occurs. 160 bool errored = false; 161 } 162 163 /// This function is called when the internal input buffer is empty. 164 // userData must be a JPEGIOHandle* 165 int stream_read_jpeg(void* pBuf, int max_bytes_to_read, bool* pEOF_flag, void* userData) @system 166 { 167 JPEGIOHandle* jio = cast(JPEGIOHandle*) userData; 168 size_t read = jio.wrapped.read(pBuf, 1, max_bytes_to_read, jio.handle); 169 if (pEOF_flag) 170 { 171 *pEOF_flag = jio.wrapped.eof(jio.handle) != 0; 172 } 173 assert(read >= 0 && read <= 0x7fff_ffff); 174 return cast(int) read; 175 } 176 177 // Note: context is a user pointer on a JPEGIOHandle. 178 void stb_stream_write(void *context, const(void)* data, int size) @system 179 { 180 JPEGIOHandle* jio = cast(JPEGIOHandle*) context; 181 182 if (jio.errored) 183 return; 184 185 size_t written = jio.wrapped.write(data, 1, size, jio.handle); 186 if (written != size) 187 jio.errored = true; // poison the JPEGIOHandleB 188 }