1 /** 2 GIF support. 3 4 Copyright: Copyright Guillaume Piolat 2023 5 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost License 1.0) 6 */ 7 module gamut.plugins.gif; 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.plugin; 16 import gamut.image; 17 import gamut.internals.errors; 18 import gamut.internals.types; 19 20 version(decodeGIF) import gamut.codecs.gif; 21 version(encodeGIF) import gamut.codecs.msf_gif; 22 23 24 ImageFormatPlugin makeGIFPlugin() 25 { 26 ImageFormatPlugin p; 27 p.format = "GIF"; 28 p.extensionList = "gif"; 29 p.mimeTypes = "image/gif"; 30 version(decodeGIF) 31 p.loadProc = &loadGIF; 32 else 33 p.loadProc = null; 34 version(encodeGIF) 35 p.saveProc = &saveGIF; 36 else 37 p.saveProc = null; 38 p.detectProc = &detectGIF; 39 return p; 40 } 41 42 bool detectGIF(IOStream *io, IOHandle handle) @trusted 43 { 44 static immutable ubyte[6] gif87Signature = [0x47, 0x49, 0x46, 0x38, 0x37, 0x61]; 45 if (fileIsStartingWithSignature(io, handle, gif87Signature)) 46 return true; 47 48 static immutable ubyte[6] gif89Signature = [0x47, 0x49, 0x46, 0x38, 0x39, 0x61]; 49 if (fileIsStartingWithSignature(io, handle, gif89Signature)) 50 return true; 51 52 return false; 53 } 54 55 56 version(decodeGIF) 57 void loadGIF(ref Image image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted 58 { 59 { 60 // Note: in that first opening, the whole file is scanned to get the number of frames. 61 GIFDecoder decoder; 62 bool err; 63 decoder.open(io, handle, &err); 64 if (err) 65 { 66 image.error(kStrImageDecodingFailed); 67 return; 68 } 69 70 image._resolutionY = GAMUT_UNKNOWN_RESOLUTION; 71 image._pixelAspectRatio = decoder.getPixelAspectRatio(); 72 73 // TODO: store decoder.FPS() somewhere, need metadata 74 75 // Create destination image. 76 // MAYDO: could use scanline conversion to create directly in l8/la8/rgb8, but limited value. 77 image.createLayeredNoInit(decoder.width, 78 decoder.height, 79 decoder.numLayers, 80 PixelType.rgba8, 81 cast(LayoutConstraints) flags); 82 83 for (int layerIndex = 0; layerIndex < decoder.numLayers; ++layerIndex) 84 { 85 bool gotOneFrame; 86 int dummy; 87 Image layerN = image.layer(layerIndex); 88 decoder.decodeNextFrame(&gotOneFrame, 89 &layerN, 90 &dummy, 91 &err); 92 if (err || !gotOneFrame) 93 { 94 image.error(kStrImageDecodingFailed); 95 return; 96 } 97 } 98 99 } // clear decoder 100 101 // Convert to target type and constraints. 102 image.convertTo(applyLoadFlags(image._type, flags), cast(LayoutConstraints) flags); 103 } 104 105 version(encodeGIF) 106 bool saveGIF(ref const(Image) image, IOStream *io, IOHandle handle, int page, int flags, void *data) @trusted 107 { 108 if (page != 0) 109 return false; 110 111 MsfGifState gifState; 112 if (!msf_gif_begin(&gifState, image.width, image.height)) 113 return false; 114 115 // 0-frames .gif not supported (not sure if possible) 116 if (image.layers == 0) 117 return false; 118 119 // Only pixelType rgba8 is supported! 120 PixelType type = image.type(); 121 if (type != PixelType.rgba8) 122 return false; 123 124 // MAYDO: could support more types by converting scanlines on-the-fly? 125 126 for (int layerIndex = 0; layerIndex < image.layers; ++layerIndex) 127 { 128 const(Image) layer = image.layer(layerIndex); 129 130 const(ubyte)* pixelData = cast(const(ubyte)*)layer.scanptr(0); 131 int centiSecondsPerFame = 7; 132 int maxBitDepth = 16; 133 int pitchInBytes = image.pitchInBytes(); 134 if (!msf_gif_frame(&gifState, pixelData, centiSecondsPerFame, maxBitDepth, pitchInBytes)) 135 { 136 return false; 137 } 138 } 139 MsfGifResult result = msf_gif_end(&gifState); 140 141 // PERF: pass IO directly, use that to write data 142 if (1 != io.write(result.data, result.dataSize, 1, handle)) 143 return false; 144 145 msf_gif_free(result); 146 return true; 147 }