1 /**
2 QOI 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.qoi;
8
9 nothrow @nogc @safe:
10
11 import core.stdc.stdlib: malloc, free, realloc;
12 import core.stdc.string: memcpy;
13 import gamut.types;
14 import gamut.io;
15 import gamut.image;
16 import gamut.plugin;
17 import gamut.internals.errors;
18 import gamut.internals.types;
19
20 version(decodeQOI)
21 import gamut.codecs.qoi;
22 else version(encodeQOI)
23 import gamut.codecs.qoi;
24
25 ImageFormatPlugin makeQOIPlugin()
26 {
27 ImageFormatPlugin p;
28 p.format = "QOI";
29 p.extensionList = "qoi";
30
31 // Discussion: https://github.com/phoboslab/qoi/issues/167#issuecomment-1117240154
32 p.mimeTypes = "image/qoi";
33
34 version(decodeQOI)
35 p.loadProc = &loadQOI;
36 else
37 p.loadProc = null;
38 version(encodeQOI)
39 p.saveProc = &saveQOI;
40 else
41 p.saveProc = null;
42 p.detectProc = &detectQOI;
43 return p;
44 }
45
46
47 version(decodeQOI)
48 void loadQOI(ref Image image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
49 {
50 // Read all available bytes from input
51 // This is temporary.
52
53 // Find length of input
54 if (io.seek(handle, 0, SEEK_END) != 0)
55 {
56 image.error(kStrImageDecodingIOFailure);
57 return;
58 }
59
60 int len = cast(int) io.tell(handle); // works, see io.d for why
61
62 if (!io.rewind(handle))
63 {
64 image.error(kStrImageDecodingIOFailure);
65 return;
66 }
67
68 ubyte* buf = cast(ubyte*) malloc(len);
69 if (buf is null)
70 {
71 image.error(kStrImageDecodingMallocFailure);
72 return;
73 }
74 scope(exit) free(buf);
75
76 int requestedComp = computeRequestedImageComponents(flags);
77 if (requestedComp == 0) // error
78 {
79 image.error(kStrInvalidFlags);
80 return;
81 }
82
83 // QOI decoder can't decode to greyscale or greyscale + alpha, but it can decode to RGB/RGBA
84 if (requestedComp == -1 || requestedComp == 1 || requestedComp == 2)
85 requestedComp = 0; // auto
86
87 ubyte* decoded;
88 qoi_desc desc;
89
90 // read all input at once.
91 if (len != io.read(buf, 1, len, handle))
92 {
93 image.error(kStrImageDecodingIOFailure);
94 return;
95 }
96
97 decoded = cast(ubyte*) qoi_decode(buf, len, &desc, requestedComp);
98 assert(decoded);
99 if (decoded is null)
100 {
101 image.error(kStrImageDecodingFailed);
102 return;
103 }
104
105 if (!imageIsValidSize(desc.width, desc.height))
106 {
107 image.error(kStrImageTooLarge);
108 free(decoded);
109 return;
110 }
111
112 // TODO: support desc.colorspace information
113
114 image._allocArea = decoded;
115 image._data = decoded;
116 image._width = desc.width;
117 image._height = desc.height;
118
119 int decodedComp = (requestedComp == 0) ? desc.channels : requestedComp;
120
121 if (decodedComp == 3)
122 image._type = PixelType.rgb8;
123 else if (decodedComp == 4)
124 image._type = PixelType.rgba8;
125 else
126 {
127 // QOI with channel different from 3 or 4 is impossible.
128 assert(false);
129 }
130
131 image._pitch = desc.channels * desc.width;
132 image._pixelAspectRatio = GAMUT_UNKNOWN_ASPECT_RATIO;
133 image._resolutionY = GAMUT_UNKNOWN_RESOLUTION;
134 image._layoutConstraints = 0; // no particular constraint followed in QOI decoder.
135
136 // Convert to target type and constraints
137 image.convertTo(applyLoadFlags(image._type, flags), cast(LayoutConstraints) flags);
138 }
139
140
141 bool detectQOI(IOStream *io, IOHandle handle) @trusted
142 {
143 static immutable ubyte[4] qoiSignature = [0x71, 0x6f, 0x69, 0x66]; // "qoif"
144 return fileIsStartingWithSignature(io, handle, qoiSignature);
145 }
146
147 version(encodeQOI)
148 bool saveQOI(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
149 {
150 if (page != 0)
151 return false;
152
153 qoi_desc desc;
154 desc.width = image._width;
155 desc.height = image._height;
156 desc.pitchBytes = image._pitch;
157 desc.colorspace = QOI_SRGB; // FUTURE: support other colorspace somehow, or at least fail if not SRGB
158
159 switch (image._type)
160 {
161 case PixelType.rgb8: desc.channels = 3; break;
162 case PixelType.rgba8: desc.channels = 4; break;
163 default:
164 {
165 int a = 0;
166 return false; // not supported
167 }
168 }
169
170 int qoilen;
171 ubyte* encoded = cast(ubyte*) qoi_encode(image._data, &desc, &qoilen);
172 if (encoded == null)
173 return false;
174 scope(exit) free(encoded);
175
176 // Write all output at once. This is rather bad, could be done progressively.
177 // PERF: adapt qoi writer to output in our own buffer directly.
178 if (qoilen != io.write(encoded, 1, qoilen, handle))
179 return false;
180
181 return true;
182 }