The OpenD Programming Language

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(1, 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     image._layerCount = 1;
136     image._layerOffset = 0;
137 
138     // Convert to target type and constraints
139     image.convertTo(applyLoadFlags(image._type, flags), cast(LayoutConstraints) flags);
140 }
141 
142 
143 bool detectQOI(IOStream *io, IOHandle handle) @trusted
144 {
145     static immutable ubyte[4] qoiSignature = [0x71, 0x6f, 0x69, 0x66]; // "qoif"
146     return fileIsStartingWithSignature(io, handle, qoiSignature);
147 }
148 
149 version(encodeQOI)
150 bool saveQOI(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted
151 {
152     if (page != 0)
153         return false;
154 
155     qoi_desc desc;
156     desc.width = image._width;
157     desc.height = image._height;
158     desc.pitchBytes = image._pitch;
159     desc.colorspace = QOI_SRGB; // FUTURE: support other colorspace somehow, or at least fail if not SRGB
160         
161     switch (image._type)
162     {
163         case PixelType.rgb8:  desc.channels = 3; break;
164         case PixelType.rgba8: desc.channels = 4; break;
165         default: 
166             {
167                 int a = 0;
168                 return false; // not supported
169             }
170     }
171         
172     int qoilen;
173     ubyte* encoded = cast(ubyte*) qoi_encode(image._data, &desc, &qoilen);
174     if (encoded == null)
175         return false;
176     scope(exit) free(encoded);
177 
178     // Write all output at once. This is rather bad, could be done progressively.
179     // PERF: adapt qoi writer to output in our own buffer directly.
180     if (qoilen != io.write(encoded, 1, qoilen, handle))
181         return false;
182 
183     return true;
184 }