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 }