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(width, height))
76     {
77         image.error(kStrImageTooLarge);
78         free(decoded.ptr);
79         return;
80     }
81 
82     image._width = width;
83     image._height = height;
84     image._allocArea = decoded.ptr;
85     image._data = decoded.ptr;
86     image._pitch = width * actualComp;
87     image._pixelAspectRatio = pixelAspectRatio == -1 ? GAMUT_UNKNOWN_ASPECT_RATIO : pixelAspectRatio;
88     image._resolutionY = dotsPerInchY == -1 ? GAMUT_UNKNOWN_RESOLUTION : dotsPerInchY;
89     image._layoutConstraints = LAYOUT_DEFAULT; // JPEG decoder follow no particular constraints (TODO?)
90 
91     switch (actualComp)
92     {
93         case 1: image._type = PixelType.l8; break;
94         case 3: image._type = PixelType.rgb8; break;
95         case 4: image._type = PixelType.rgba8; break;
96         default:
97     }
98 
99     // Convert to target type and constraints
100     image.convertTo(applyLoadFlags(image._type, flags), cast(LayoutConstraints) flags);
101 }
102 
103 bool detectJPEG(IOStream *io, IOHandle handle) @trusted
104 {
105     static immutable ubyte[2] jpegSignature = [0xFF, 0xD8];
106     return fileIsStartingWithSignature(io, handle, jpegSignature);
107 }
108 
109 version(encodeJPEG)
110 bool saveJPEG(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
111 {
112     if (page != 0)
113         return false;
114 
115     int components;
116 
117     switch (image._type)
118     {
119         case PixelType.l8:
120             components = 1; break;
121         case PixelType.rgb8:
122             components = 3; 
123             break;
124         case PixelType.rgba8:
125             return false; // stb would throw away alpha
126         default:
127             return false;
128     }
129 
130     JPEGIOHandle jio;
131     jio.wrapped = io;
132     jio.handle = handle;
133 
134     void* userPointer = cast(void*)&jio;
135 
136     int quality = 90; // TODO: option to choose that.
137 
138     int res = stbi_write_jpg_to_func(&stb_stream_write, userPointer, 
139                                         image._width, 
140                                         image._height, 
141                                         components, 
142                                         image._data, quality);
143 
144     return res == 1 && !jio.errored;
145 }
146 
147 private:
148 
149 
150 struct JPEGIOHandle
151 {
152     IOStream* wrapped;
153     IOHandle handle;
154 
155     // stb_image_write doesn't check errors for write, so keep a flag and start ignoring output if
156     // an I/O error occurs.
157     bool errored = false;
158 }
159 
160 /// This function is called when the internal input buffer is empty.
161 // userData must be a JPEGIOHandle*
162 int stream_read_jpeg(void* pBuf, int max_bytes_to_read, bool* pEOF_flag, void* userData) @system
163 {
164     JPEGIOHandle* jio = cast(JPEGIOHandle*) userData;
165     size_t read = jio.wrapped.read(pBuf, 1, max_bytes_to_read, jio.handle);
166     if (pEOF_flag)
167     {
168         *pEOF_flag = jio.wrapped.eof(jio.handle) != 0;
169     }
170     assert(read >= 0 && read <= 0x7fff_ffff);
171     return cast(int) read;
172 }
173 
174 // Note: context is a user pointer on a JPEGIOHandle.
175 void stb_stream_write(void *context, const(void)* data, int size) @system
176 {    
177     JPEGIOHandle* jio = cast(JPEGIOHandle*) context;
178 
179     if (jio.errored)
180         return;
181 
182     size_t written = jio.wrapped.write(data, 1, size, jio.handle);
183     if (written != size)
184         jio.errored = true; // poison the JPEGIOHandleB
185 }