├── .travis.yml ├── tests ├── pngsuite │ ├── basi0g08.png │ ├── basi2c08.png │ ├── basi3p08.png │ ├── basi4a08.png │ ├── basi6a08.png │ ├── basn0g08.png │ ├── basn2c08.png │ ├── basn3p08.png │ ├── basn4a08.png │ ├── basn6a08.png │ ├── PngSuite.LICENSE │ └── PngSuite.README ├── pngsuite-bmp │ ├── basi0g08.bmp │ ├── basi2c08.bmp │ ├── basi3p08.bmp │ ├── basi4a08.bmp │ ├── basi6a08.bmp │ ├── basn0g08.bmp │ ├── basn2c08.bmp │ ├── basn3p08.bmp │ ├── basn4a08.bmp │ └── basn6a08.bmp └── pngsuite-tga │ ├── basi0g08.tga │ ├── basi2c08.tga │ ├── basi3p08.tga │ ├── basi4a08.tga │ ├── basi6a08.tga │ ├── basn0g08.tga │ ├── basn2c08.tga │ ├── basn3p08.tga │ ├── basn4a08.tga │ └── basn6a08.tga ├── dub.sdl ├── README.md ├── LICENSE └── imageformats ├── package.d ├── tga.d ├── bmp.d ├── png.d └── jpeg.d /.travis.yml: -------------------------------------------------------------------------------- 1 | language: d 2 | -------------------------------------------------------------------------------- /tests/pngsuite/basi0g08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite/basi0g08.png -------------------------------------------------------------------------------- /tests/pngsuite/basi2c08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite/basi2c08.png -------------------------------------------------------------------------------- /tests/pngsuite/basi3p08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite/basi3p08.png -------------------------------------------------------------------------------- /tests/pngsuite/basi4a08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite/basi4a08.png -------------------------------------------------------------------------------- /tests/pngsuite/basi6a08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite/basi6a08.png -------------------------------------------------------------------------------- /tests/pngsuite/basn0g08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite/basn0g08.png -------------------------------------------------------------------------------- /tests/pngsuite/basn2c08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite/basn2c08.png -------------------------------------------------------------------------------- /tests/pngsuite/basn3p08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite/basn3p08.png -------------------------------------------------------------------------------- /tests/pngsuite/basn4a08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite/basn4a08.png -------------------------------------------------------------------------------- /tests/pngsuite/basn6a08.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite/basn6a08.png -------------------------------------------------------------------------------- /tests/pngsuite-bmp/basi0g08.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-bmp/basi0g08.bmp -------------------------------------------------------------------------------- /tests/pngsuite-bmp/basi2c08.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-bmp/basi2c08.bmp -------------------------------------------------------------------------------- /tests/pngsuite-bmp/basi3p08.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-bmp/basi3p08.bmp -------------------------------------------------------------------------------- /tests/pngsuite-bmp/basi4a08.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-bmp/basi4a08.bmp -------------------------------------------------------------------------------- /tests/pngsuite-bmp/basi6a08.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-bmp/basi6a08.bmp -------------------------------------------------------------------------------- /tests/pngsuite-bmp/basn0g08.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-bmp/basn0g08.bmp -------------------------------------------------------------------------------- /tests/pngsuite-bmp/basn2c08.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-bmp/basn2c08.bmp -------------------------------------------------------------------------------- /tests/pngsuite-bmp/basn3p08.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-bmp/basn3p08.bmp -------------------------------------------------------------------------------- /tests/pngsuite-bmp/basn4a08.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-bmp/basn4a08.bmp -------------------------------------------------------------------------------- /tests/pngsuite-bmp/basn6a08.bmp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-bmp/basn6a08.bmp -------------------------------------------------------------------------------- /tests/pngsuite-tga/basi0g08.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-tga/basi0g08.tga -------------------------------------------------------------------------------- /tests/pngsuite-tga/basi2c08.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-tga/basi2c08.tga -------------------------------------------------------------------------------- /tests/pngsuite-tga/basi3p08.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-tga/basi3p08.tga -------------------------------------------------------------------------------- /tests/pngsuite-tga/basi4a08.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-tga/basi4a08.tga -------------------------------------------------------------------------------- /tests/pngsuite-tga/basi6a08.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-tga/basi6a08.tga -------------------------------------------------------------------------------- /tests/pngsuite-tga/basn0g08.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-tga/basn0g08.tga -------------------------------------------------------------------------------- /tests/pngsuite-tga/basn2c08.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-tga/basn2c08.tga -------------------------------------------------------------------------------- /tests/pngsuite-tga/basn3p08.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-tga/basn3p08.tga -------------------------------------------------------------------------------- /tests/pngsuite-tga/basn4a08.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-tga/basn4a08.tga -------------------------------------------------------------------------------- /tests/pngsuite-tga/basn6a08.tga: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tjhann/imageformats/HEAD/tests/pngsuite-tga/basn6a08.tga -------------------------------------------------------------------------------- /dub.sdl: -------------------------------------------------------------------------------- 1 | name "imageformats" 2 | description "Decoders for PNG, TGA, BMP, JPEG and encoders for PNG, TGA, BMP." 3 | authors "Tero Hänninen" 4 | license "BSL-1.0" 5 | targetName "imageformats" 6 | sourcePaths "imageformats" 7 | importPaths "." 8 | -------------------------------------------------------------------------------- /tests/pngsuite/PngSuite.LICENSE: -------------------------------------------------------------------------------- 1 | PngSuite 2 | -------- 3 | 4 | Permission to use, copy, modify and distribute these images for any 5 | purpose and without fee is hereby granted. 6 | 7 | 8 | (c) Willem van Schaik, 1996, 2011 9 | 10 | -------------------------------------------------------------------------------- /tests/pngsuite/PngSuite.README: -------------------------------------------------------------------------------- 1 | PNGSUITE 2 | ---------------- 3 | 4 | testset for PNG-(de)coders 5 | created by Willem van Schaik 6 | ------------------------------------ 7 | 8 | This is a collection of graphics images created to test the png applications 9 | like viewers, converters and editors. All (as far as that is possible) 10 | formats supported by the PNG standard are represented. 11 | 12 | The suite consists of the following files: 13 | 14 | - PngSuite.README - this file 15 | - PngSuite.LICENSE - the PngSuite is freeware 16 | - PngSuite.png - image with PngSuite logo 17 | - PngSuite.tgz - archive of all PNG testfiles 18 | - PngSuite.zip - same in .zip format for PCs 19 | 20 | 21 | -------- 22 | (c) Willem van Schaik 23 | willem@schaik.com 24 | Calgary, April 2011 25 | 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # imageformats [![Build Status](https://travis-ci.org/lgvz/imageformats.svg)](https://travis-ci.org/lgvz/imageformats) 2 | 3 | - Returned image data is 8-bit except PNG can also return 16-bit. 4 | - Image data can be converted to Y, YA, RGB or RGBA. 5 | - There's a `@nogc` remake: [imagefmt](https://github.com/tjhann/imagefmt) 6 | 7 | **Decoders:** 8 | - PNG. 8-bit and 16-bit interlaced and paletted (+`tRNS` chunk) 9 | - TGA. 8-bit non-paletted 10 | - BMP. 8-bit uncompressed 11 | - JPEG. baseline 12 | 13 | **Encoders:** 14 | - PNG. 8-bit non-paletted non-interlaced 15 | - TGA. 8-bit non-paletted rle-compressed 16 | - BMP. 8-bit uncompressed 17 | 18 | ```D 19 | import imageformats; 20 | 21 | void main() { 22 | IFImage i0 = read_image("peruna.png"); 23 | IFImage i1 = read_image("peruna.png", ColFmt.YA); // convert 24 | IFImage i2 = read_image("peruna.png", ColFmt.RGB); 25 | 26 | write_image("peruna.tga", i0.w, i0.h, i0.pixels); 27 | write_image("peruna.tga", i0.w, i0.h, i0.pixels, ColFmt.RGBA); 28 | 29 | int w, h, chans; 30 | read_image_info("peruna.png", w, h, chans); 31 | 32 | // format specific functions 33 | PNG_Header hdr = read_png_header("peruna.png"); 34 | IFImage i3 = read_jpeg("porkkana.jpg"); 35 | write_tga("porkkana.tga", i3.w, i3.h, i3.pixels); 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /imageformats/package.d: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2014-2018 Tero Hänninen 2 | // Boost Software License - Version 1.0 - August 17th, 2003 3 | module imageformats; 4 | 5 | import std.stdio : File, SEEK_SET, SEEK_CUR, SEEK_END; 6 | import std.string : toLower, lastIndexOf; 7 | import std.typecons : scoped; 8 | public import imageformats.png; 9 | public import imageformats.tga; 10 | public import imageformats.bmp; 11 | public import imageformats.jpeg; 12 | 13 | /// Image with 8-bit channels. Top-left corner at (0, 0). 14 | struct IFImage { 15 | /// width 16 | int w; 17 | /// height 18 | int h; 19 | /// channels 20 | ColFmt c; 21 | /// buffer 22 | ubyte[] pixels; 23 | } 24 | 25 | /// Image with 16-bit channels. Top-left corner at (0, 0). 26 | struct IFImage16 { 27 | /// width 28 | int w; 29 | /// height 30 | int h; 31 | /// channels 32 | ColFmt c; 33 | /// buffer 34 | ushort[] pixels; 35 | } 36 | 37 | /// Color format which you can pass to the read and write functions. 38 | enum ColFmt { 39 | Y = 1, /// Gray 40 | YA = 2, /// Gray + Alpha 41 | RGB = 3, /// Truecolor 42 | RGBA = 4, /// Truecolor + Alpha 43 | } 44 | 45 | /// Reads an image from file. req_chans defines the format of returned image 46 | /// (you can use ColFmt here). 47 | IFImage read_image(in char[] file, long req_chans = 0) { 48 | auto reader = scoped!FileReader(file); 49 | return read_image_from_reader(reader, req_chans); 50 | } 51 | 52 | /// Reads an image from a buffer. req_chans defines the format of returned 53 | /// image (you can use ColFmt here). 54 | IFImage read_image_from_mem(in ubyte[] source, long req_chans = 0) { 55 | auto reader = scoped!MemReader(source); 56 | return read_image_from_reader(reader, req_chans); 57 | } 58 | 59 | /// Writes an image to file. req_chans defines the format the image is saved in 60 | /// (you can use ColFmt here). 61 | void write_image(in char[] file, long w, long h, in ubyte[] data, long req_chans = 0) { 62 | const char[] ext = extract_extension_lowercase(file); 63 | 64 | void function(Writer, long, long, in ubyte[], long) write_image; 65 | switch (ext) { 66 | case "png": write_image = &write_png; break; 67 | case "tga": write_image = &write_tga; break; 68 | case "bmp": write_image = &write_bmp; break; 69 | default: throw new ImageIOException("unknown image extension/type"); 70 | } 71 | auto writer = scoped!FileWriter(file); 72 | write_image(writer, w, h, data, req_chans); 73 | } 74 | 75 | /// Returns width, height and color format information via w, h and chans. 76 | /// If number of channels is unknown chans is set to zero, otherwise chans 77 | /// values map to those of ColFmt. 78 | void read_image_info(in char[] file, out int w, out int h, out int chans) { 79 | auto reader = scoped!FileReader(file); 80 | try { 81 | return read_png_info(reader, w, h, chans); 82 | } catch (Throwable) { 83 | reader.seek(0, SEEK_SET); 84 | } 85 | try { 86 | return read_jpeg_info(reader, w, h, chans); 87 | } catch (Throwable) { 88 | reader.seek(0, SEEK_SET); 89 | } 90 | try { 91 | return read_bmp_info(reader, w, h, chans); 92 | } catch (Throwable) { 93 | reader.seek(0, SEEK_SET); 94 | } 95 | try { 96 | return read_tga_info(reader, w, h, chans); 97 | } catch (Throwable) { 98 | reader.seek(0, SEEK_SET); 99 | } 100 | throw new ImageIOException("unknown image type"); 101 | } 102 | 103 | /// Thrown from all the functions... 104 | class ImageIOException : Exception { 105 | @safe pure const 106 | this(string msg, string file = __FILE__, size_t line = __LINE__) { 107 | super(msg, file, line); 108 | } 109 | } 110 | 111 | private: 112 | 113 | IFImage read_image_from_reader(Reader reader, long req_chans) { 114 | if (detect_png(reader)) return read_png(reader, req_chans); 115 | if (detect_jpeg(reader)) return read_jpeg(reader, req_chans); 116 | if (detect_bmp(reader)) return read_bmp(reader, req_chans); 117 | if (detect_tga(reader)) return read_tga(reader, req_chans); 118 | throw new ImageIOException("unknown image type"); 119 | } 120 | 121 | // -------------------------------------------------------------------------------- 122 | // Conversions 123 | 124 | package enum _ColFmt : int { 125 | Unknown = 0, 126 | Y = 1, 127 | YA, 128 | RGB, 129 | RGBA, 130 | BGR, 131 | BGRA, 132 | } 133 | 134 | package alias LineConv(T) = void function(in T[] src, T[] tgt); 135 | 136 | package LineConv!T get_converter(T)(long src_chans, long tgt_chans) pure { 137 | long combo(long a, long b) pure nothrow { return a*16 + b; } 138 | 139 | if (src_chans == tgt_chans) 140 | return ©_line!T; 141 | 142 | switch (combo(src_chans, tgt_chans)) with (_ColFmt) { 143 | case combo(Y, YA) : return &Y_to_YA!T; 144 | case combo(Y, RGB) : return &Y_to_RGB!T; 145 | case combo(Y, RGBA) : return &Y_to_RGBA!T; 146 | case combo(Y, BGR) : return &Y_to_BGR!T; 147 | case combo(Y, BGRA) : return &Y_to_BGRA!T; 148 | case combo(YA, Y) : return &YA_to_Y!T; 149 | case combo(YA, RGB) : return &YA_to_RGB!T; 150 | case combo(YA, RGBA) : return &YA_to_RGBA!T; 151 | case combo(YA, BGR) : return &YA_to_BGR!T; 152 | case combo(YA, BGRA) : return &YA_to_BGRA!T; 153 | case combo(RGB, Y) : return &RGB_to_Y!T; 154 | case combo(RGB, YA) : return &RGB_to_YA!T; 155 | case combo(RGB, RGBA) : return &RGB_to_RGBA!T; 156 | case combo(RGB, BGR) : return &RGB_to_BGR!T; 157 | case combo(RGB, BGRA) : return &RGB_to_BGRA!T; 158 | case combo(RGBA, Y) : return &RGBA_to_Y!T; 159 | case combo(RGBA, YA) : return &RGBA_to_YA!T; 160 | case combo(RGBA, RGB) : return &RGBA_to_RGB!T; 161 | case combo(RGBA, BGR) : return &RGBA_to_BGR!T; 162 | case combo(RGBA, BGRA) : return &RGBA_to_BGRA!T; 163 | case combo(BGR, Y) : return &BGR_to_Y!T; 164 | case combo(BGR, YA) : return &BGR_to_YA!T; 165 | case combo(BGR, RGB) : return &BGR_to_RGB!T; 166 | case combo(BGR, RGBA) : return &BGR_to_RGBA!T; 167 | case combo(BGRA, Y) : return &BGRA_to_Y!T; 168 | case combo(BGRA, YA) : return &BGRA_to_YA!T; 169 | case combo(BGRA, RGB) : return &BGRA_to_RGB!T; 170 | case combo(BGRA, RGBA) : return &BGRA_to_RGBA!T; 171 | default : throw new ImageIOException("internal error"); 172 | } 173 | } 174 | 175 | void copy_line(T)(in T[] src, T[] tgt) pure nothrow { 176 | tgt[0..$] = src[0..$]; 177 | } 178 | 179 | T luminance(T)(T r, T g, T b) pure nothrow { 180 | return cast(T) (0.21*r + 0.64*g + 0.15*b); // somewhat arbitrary weights 181 | } 182 | 183 | void Y_to_YA(T)(in T[] src, T[] tgt) pure nothrow { 184 | for (size_t k, t; k < src.length; k+=1, t+=2) { 185 | tgt[t] = src[k]; 186 | tgt[t+1] = T.max; 187 | } 188 | } 189 | 190 | alias Y_to_BGR = Y_to_RGB; 191 | void Y_to_RGB(T)(in T[] src, T[] tgt) pure nothrow { 192 | for (size_t k, t; k < src.length; k+=1, t+=3) 193 | tgt[t .. t+3] = src[k]; 194 | } 195 | 196 | alias Y_to_BGRA = Y_to_RGBA; 197 | void Y_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow { 198 | for (size_t k, t; k < src.length; k+=1, t+=4) { 199 | tgt[t .. t+3] = src[k]; 200 | tgt[t+3] = T.max; 201 | } 202 | } 203 | 204 | void YA_to_Y(T)(in T[] src, T[] tgt) pure nothrow { 205 | for (size_t k, t; k < src.length; k+=2, t+=1) 206 | tgt[t] = src[k]; 207 | } 208 | 209 | alias YA_to_BGR = YA_to_RGB; 210 | void YA_to_RGB(T)(in T[] src, T[] tgt) pure nothrow { 211 | for (size_t k, t; k < src.length; k+=2, t+=3) 212 | tgt[t .. t+3] = src[k]; 213 | } 214 | 215 | alias YA_to_BGRA = YA_to_RGBA; 216 | void YA_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow { 217 | for (size_t k, t; k < src.length; k+=2, t+=4) { 218 | tgt[t .. t+3] = src[k]; 219 | tgt[t+3] = src[k+1]; 220 | } 221 | } 222 | 223 | void RGB_to_Y(T)(in T[] src, T[] tgt) pure nothrow { 224 | for (size_t k, t; k < src.length; k+=3, t+=1) 225 | tgt[t] = luminance(src[k], src[k+1], src[k+2]); 226 | } 227 | 228 | void RGB_to_YA(T)(in T[] src, T[] tgt) pure nothrow { 229 | for (size_t k, t; k < src.length; k+=3, t+=2) { 230 | tgt[t] = luminance(src[k], src[k+1], src[k+2]); 231 | tgt[t+1] = T.max; 232 | } 233 | } 234 | 235 | void RGB_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow { 236 | for (size_t k, t; k < src.length; k+=3, t+=4) { 237 | tgt[t .. t+3] = src[k .. k+3]; 238 | tgt[t+3] = T.max; 239 | } 240 | } 241 | 242 | void RGBA_to_Y(T)(in T[] src, T[] tgt) pure nothrow { 243 | for (size_t k, t; k < src.length; k+=4, t+=1) 244 | tgt[t] = luminance(src[k], src[k+1], src[k+2]); 245 | } 246 | 247 | void RGBA_to_YA(T)(in T[] src, T[] tgt) pure nothrow { 248 | for (size_t k, t; k < src.length; k+=4, t+=2) { 249 | tgt[t] = luminance(src[k], src[k+1], src[k+2]); 250 | tgt[t+1] = src[k+3]; 251 | } 252 | } 253 | 254 | void RGBA_to_RGB(T)(in T[] src, T[] tgt) pure nothrow { 255 | for (size_t k, t; k < src.length; k+=4, t+=3) 256 | tgt[t .. t+3] = src[k .. k+3]; 257 | } 258 | 259 | void BGR_to_Y(T)(in T[] src, T[] tgt) pure nothrow { 260 | for (size_t k, t; k < src.length; k+=3, t+=1) 261 | tgt[t] = luminance(src[k+2], src[k+1], src[k+1]); 262 | } 263 | 264 | void BGR_to_YA(T)(in T[] src, T[] tgt) pure nothrow { 265 | for (size_t k, t; k < src.length; k+=3, t+=2) { 266 | tgt[t] = luminance(src[k+2], src[k+1], src[k+1]); 267 | tgt[t+1] = T.max; 268 | } 269 | } 270 | 271 | alias RGB_to_BGR = BGR_to_RGB; 272 | void BGR_to_RGB(T)(in T[] src, T[] tgt) pure nothrow { 273 | for (size_t k; k < src.length; k+=3) { 274 | tgt[k ] = src[k+2]; 275 | tgt[k+1] = src[k+1]; 276 | tgt[k+2] = src[k ]; 277 | } 278 | } 279 | 280 | alias RGB_to_BGRA = BGR_to_RGBA; 281 | void BGR_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow { 282 | for (size_t k, t; k < src.length; k+=3, t+=4) { 283 | tgt[t ] = src[k+2]; 284 | tgt[t+1] = src[k+1]; 285 | tgt[t+2] = src[k ]; 286 | tgt[t+3] = T.max; 287 | } 288 | } 289 | 290 | void BGRA_to_Y(T)(in T[] src, T[] tgt) pure nothrow { 291 | for (size_t k, t; k < src.length; k+=4, t+=1) 292 | tgt[t] = luminance(src[k+2], src[k+1], src[k]); 293 | } 294 | 295 | void BGRA_to_YA(T)(in T[] src, T[] tgt) pure nothrow { 296 | for (size_t k, t; k < src.length; k+=4, t+=2) { 297 | tgt[t] = luminance(src[k+2], src[k+1], src[k]); 298 | tgt[t+1] = T.max; 299 | } 300 | } 301 | 302 | alias RGBA_to_BGR = BGRA_to_RGB; 303 | void BGRA_to_RGB(T)(in T[] src, T[] tgt) pure nothrow { 304 | for (size_t k, t; k < src.length; k+=4, t+=3) { 305 | tgt[t ] = src[k+2]; 306 | tgt[t+1] = src[k+1]; 307 | tgt[t+2] = src[k ]; 308 | } 309 | } 310 | 311 | alias RGBA_to_BGRA = BGRA_to_RGBA; 312 | void BGRA_to_RGBA(T)(in T[] src, T[] tgt) pure nothrow { 313 | for (size_t k, t; k < src.length; k+=4, t+=4) { 314 | tgt[t ] = src[k+2]; 315 | tgt[t+1] = src[k+1]; 316 | tgt[t+2] = src[k ]; 317 | tgt[t+3] = src[k+3]; 318 | } 319 | } 320 | 321 | // -------------------------------------------------------------------------------- 322 | 323 | package interface Reader { 324 | void readExact(ubyte[], size_t); 325 | void seek(ptrdiff_t, int); 326 | } 327 | 328 | package interface Writer { 329 | void rawWrite(in ubyte[]); 330 | void flush(); 331 | } 332 | 333 | package class FileReader : Reader { 334 | this(in char[] filename) { 335 | this(File(filename.idup, "rb")); 336 | } 337 | 338 | this(File f) { 339 | if (!f.isOpen) throw new ImageIOException("File not open"); 340 | this.f = f; 341 | } 342 | 343 | void readExact(ubyte[] buffer, size_t bytes) { 344 | auto slice = this.f.rawRead(buffer[0..bytes]); 345 | if (slice.length != bytes) 346 | throw new Exception("not enough data"); 347 | } 348 | 349 | void seek(ptrdiff_t offset, int origin) { this.f.seek(offset, origin); } 350 | 351 | private File f; 352 | } 353 | 354 | package class MemReader : Reader { 355 | this(in ubyte[] source) { 356 | this.source = source; 357 | } 358 | 359 | void readExact(ubyte[] buffer, size_t bytes) { 360 | if (source.length - cursor < bytes) 361 | throw new Exception("not enough data"); 362 | buffer[0..bytes] = source[cursor .. cursor+bytes]; 363 | cursor += bytes; 364 | } 365 | 366 | void seek(ptrdiff_t offset, int origin) { 367 | switch (origin) { 368 | case SEEK_SET: 369 | if (offset < 0 || source.length <= offset) 370 | throw new Exception("seek error"); 371 | cursor = offset; 372 | break; 373 | case SEEK_CUR: 374 | ptrdiff_t dst = cursor + offset; 375 | if (dst < 0 || source.length <= dst) 376 | throw new Exception("seek error"); 377 | cursor = dst; 378 | break; 379 | case SEEK_END: 380 | if (0 <= offset || source.length < -offset) 381 | throw new Exception("seek error"); 382 | cursor = cast(ptrdiff_t) source.length + offset; 383 | break; 384 | default: assert(0); 385 | } 386 | } 387 | 388 | private const ubyte[] source; 389 | private ptrdiff_t cursor; 390 | } 391 | 392 | package class FileWriter : Writer { 393 | this(in char[] filename) { 394 | this(File(filename.idup, "wb")); 395 | } 396 | 397 | this(File f) { 398 | if (!f.isOpen) throw new ImageIOException("File not open"); 399 | this.f = f; 400 | } 401 | 402 | void rawWrite(in ubyte[] block) { this.f.rawWrite(block); } 403 | void flush() { this.f.flush(); } 404 | 405 | private File f; 406 | } 407 | 408 | package class MemWriter : Writer { 409 | this() { } 410 | 411 | ubyte[] result() { return buffer; } 412 | 413 | void rawWrite(in ubyte[] block) { this.buffer ~= block; } 414 | void flush() { } 415 | 416 | private ubyte[] buffer; 417 | } 418 | 419 | const(char)[] extract_extension_lowercase(in char[] filename) { 420 | ptrdiff_t di = filename.lastIndexOf('.'); 421 | return (0 < di && di+1 < filename.length) ? filename[di+1..$].toLower() : ""; 422 | } 423 | 424 | unittest { 425 | // The TGA and BMP files are not as varied in format as the PNG files, so 426 | // not as well tested. 427 | string png_path = "tests/pngsuite/"; 428 | string tga_path = "tests/pngsuite-tga/"; 429 | string bmp_path = "tests/pngsuite-bmp/"; 430 | 431 | auto files = [ 432 | "basi0g08", // PNG image data, 32 x 32, 8-bit grayscale, interlaced 433 | "basi2c08", // PNG image data, 32 x 32, 8-bit/color RGB, interlaced 434 | "basi3p08", // PNG image data, 32 x 32, 8-bit colormap, interlaced 435 | "basi4a08", // PNG image data, 32 x 32, 8-bit gray+alpha, interlaced 436 | "basi6a08", // PNG image data, 32 x 32, 8-bit/color RGBA, interlaced 437 | "basn0g08", // PNG image data, 32 x 32, 8-bit grayscale, non-interlaced 438 | "basn2c08", // PNG image data, 32 x 32, 8-bit/color RGB, non-interlaced 439 | "basn3p08", // PNG image data, 32 x 32, 8-bit colormap, non-interlaced 440 | "basn4a08", // PNG image data, 32 x 32, 8-bit gray+alpha, non-interlaced 441 | "basn6a08", // PNG image data, 32 x 32, 8-bit/color RGBA, non-interlaced 442 | ]; 443 | 444 | foreach (file; files) { 445 | //writefln("%s", file); 446 | auto a = read_image(png_path ~ file ~ ".png", ColFmt.RGBA); 447 | auto b = read_image(tga_path ~ file ~ ".tga", ColFmt.RGBA); 448 | auto c = read_image(bmp_path ~ file ~ ".bmp", ColFmt.RGBA); 449 | assert(a.w == b.w && a.w == c.w); 450 | assert(a.h == b.h && a.h == c.h); 451 | assert(a.pixels.length == b.pixels.length && a.pixels.length == c.pixels.length); 452 | assert(a.pixels[0..$] == b.pixels[0..$], "png/tga"); 453 | assert(a.pixels[0..$] == c.pixels[0..$], "png/bmp"); 454 | } 455 | } 456 | -------------------------------------------------------------------------------- /imageformats/tga.d: -------------------------------------------------------------------------------- 1 | module imageformats.tga; 2 | 3 | import std.algorithm : min; 4 | import std.bitmanip : littleEndianToNative, nativeToLittleEndian; 5 | import std.stdio : File, SEEK_SET, SEEK_CUR; 6 | import std.typecons : scoped; 7 | import imageformats; 8 | 9 | private: 10 | 11 | /// Header of a TGA file. 12 | public struct TGA_Header { 13 | ubyte id_length; 14 | ubyte palette_type; 15 | ubyte data_type; 16 | ushort palette_start; 17 | ushort palette_length; 18 | ubyte palette_bits; 19 | ushort x_origin; 20 | ushort y_origin; 21 | ushort width; 22 | ushort height; 23 | ubyte bits_pp; 24 | ubyte flags; 25 | } 26 | 27 | /// Returns the header of a TGA file. 28 | public TGA_Header read_tga_header(in char[] filename) { 29 | auto reader = scoped!FileReader(filename); 30 | return read_tga_header(reader); 31 | } 32 | 33 | /// Reads the image header from a buffer containing a TGA image. 34 | public TGA_Header read_tga_header_from_mem(in ubyte[] source) { 35 | auto reader = scoped!MemReader(source); 36 | return read_tga_header(reader); 37 | } 38 | 39 | /// Reads a TGA image. req_chans defines the format of returned image 40 | /// (you can use ColFmt here). 41 | public IFImage read_tga(in char[] filename, long req_chans = 0) { 42 | auto reader = scoped!FileReader(filename); 43 | return read_tga(reader, req_chans); 44 | } 45 | 46 | /// Reads an image from a buffer containing a TGA image. req_chans defines the 47 | /// format of returned image (you can use ColFmt here). 48 | public IFImage read_tga_from_mem(in ubyte[] source, long req_chans = 0) { 49 | auto reader = scoped!MemReader(source); 50 | return read_tga(reader, req_chans); 51 | } 52 | 53 | /// Writes a TGA image into a file. 54 | public void write_tga(in char[] file, long w, long h, in ubyte[] data, long tgt_chans = 0) 55 | { 56 | auto writer = scoped!FileWriter(file); 57 | write_tga(writer, w, h, data, tgt_chans); 58 | } 59 | 60 | /// Writes a TGA image into a buffer. 61 | public ubyte[] write_tga_to_mem(long w, long h, in ubyte[] data, long tgt_chans = 0) { 62 | auto writer = scoped!MemWriter(); 63 | write_tga(writer, w, h, data, tgt_chans); 64 | return writer.result; 65 | } 66 | 67 | /// Returns width, height and color format information via w, h and chans. 68 | public void read_tga_info(in char[] filename, out int w, out int h, out int chans) { 69 | auto reader = scoped!FileReader(filename); 70 | return read_tga_info(reader, w, h, chans); 71 | } 72 | 73 | /// Returns width, height and color format information via w, h and chans. 74 | public void read_tga_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) { 75 | auto reader = scoped!MemReader(source); 76 | return read_tga_info(reader, w, h, chans); 77 | } 78 | 79 | // Detects whether a TGA image is readable from stream. 80 | package bool detect_tga(Reader stream) { 81 | try { 82 | auto hdr = read_tga_header(stream); 83 | return true; 84 | } catch (Throwable) { 85 | return false; 86 | } finally { 87 | stream.seek(0, SEEK_SET); 88 | } 89 | } 90 | 91 | TGA_Header read_tga_header(Reader stream) { 92 | ubyte[18] tmp = void; 93 | stream.readExact(tmp, tmp.length); 94 | 95 | TGA_Header hdr = { 96 | id_length : tmp[0], 97 | palette_type : tmp[1], 98 | data_type : tmp[2], 99 | palette_start : littleEndianToNative!ushort(tmp[3..5]), 100 | palette_length : littleEndianToNative!ushort(tmp[5..7]), 101 | palette_bits : tmp[7], 102 | x_origin : littleEndianToNative!ushort(tmp[8..10]), 103 | y_origin : littleEndianToNative!ushort(tmp[10..12]), 104 | width : littleEndianToNative!ushort(tmp[12..14]), 105 | height : littleEndianToNative!ushort(tmp[14..16]), 106 | bits_pp : tmp[16], 107 | flags : tmp[17], 108 | }; 109 | 110 | if (hdr.width < 1 || hdr.height < 1 || hdr.palette_type > 1 111 | || (hdr.palette_type == 0 && (hdr.palette_start 112 | || hdr.palette_length 113 | || hdr.palette_bits)) 114 | || (4 <= hdr.data_type && hdr.data_type <= 8) || 12 <= hdr.data_type) 115 | throw new ImageIOException("corrupt TGA header"); 116 | 117 | return hdr; 118 | } 119 | 120 | package IFImage read_tga(Reader stream, long req_chans = 0) { 121 | if (req_chans < 0 || 4 < req_chans) 122 | throw new ImageIOException("come on..."); 123 | 124 | TGA_Header hdr = read_tga_header(stream); 125 | 126 | if (hdr.width < 1 || hdr.height < 1) 127 | throw new ImageIOException("invalid dimensions"); 128 | if (hdr.flags & 0xc0) // two bits 129 | throw new ImageIOException("interlaced TGAs not supported"); 130 | if (hdr.flags & 0x10) 131 | throw new ImageIOException("right-to-left TGAs not supported"); 132 | ubyte attr_bits_pp = (hdr.flags & 0xf); 133 | if (! (attr_bits_pp == 0 || attr_bits_pp == 8)) // some set it 0 although data has 8 134 | throw new ImageIOException("only 8-bit alpha/attribute(s) supported"); 135 | if (hdr.palette_type) 136 | throw new ImageIOException("paletted TGAs not supported"); 137 | 138 | const bool rle = hdr.data_type == TGA_DataType.TrueColor_RLE // Idx_RLE 139 | || hdr.data_type == TGA_DataType.Gray_RLE; // not supported 140 | 141 | switch (hdr.data_type) with (TGA_DataType) { 142 | case TrueColor: 143 | case TrueColor_RLE: 144 | if (hdr.bits_pp != 24 && hdr.bits_pp != 32) 145 | throw new ImageIOException("not supported"); 146 | break; 147 | case Gray: 148 | case Gray_RLE: 149 | if (hdr.bits_pp != 8 && !(hdr.bits_pp == 16 && attr_bits_pp == 8)) 150 | throw new ImageIOException("not supported"); 151 | break; 152 | default: 153 | throw new ImageIOException("not supported"); 154 | } 155 | 156 | int src_chans = hdr.bits_pp / 8; 157 | 158 | if (hdr.id_length) 159 | stream.seek(hdr.id_length, SEEK_CUR); 160 | 161 | TGA_Decoder dc = { 162 | stream : stream, 163 | w : hdr.width, 164 | h : hdr.height, 165 | origin_at_top : cast(bool) (hdr.flags & 0x20), 166 | bytes_pp : hdr.bits_pp / 8, 167 | rle : rle, 168 | tgt_chans : (req_chans == 0) ? src_chans : cast(int) req_chans, 169 | }; 170 | 171 | switch (dc.bytes_pp) { 172 | case 1: dc.src_fmt = _ColFmt.Y; break; 173 | case 2: dc.src_fmt = _ColFmt.YA; break; 174 | case 3: dc.src_fmt = _ColFmt.BGR; break; 175 | case 4: dc.src_fmt = _ColFmt.BGRA; break; 176 | default: throw new ImageIOException("TGA: format not supported"); 177 | } 178 | 179 | IFImage result = { 180 | w : dc.w, 181 | h : dc.h, 182 | c : cast(ColFmt) dc.tgt_chans, 183 | pixels : decode_tga(dc), 184 | }; 185 | return result; 186 | } 187 | 188 | void write_tga(Writer stream, long w, long h, in ubyte[] data, long tgt_chans = 0) { 189 | if (w < 1 || h < 1 || ushort.max < w || ushort.max < h) 190 | throw new ImageIOException("invalid dimensions"); 191 | ulong src_chans = data.length / w / h; 192 | if (src_chans < 1 || 4 < src_chans || tgt_chans < 0 || 4 < tgt_chans) 193 | throw new ImageIOException("invalid channel count"); 194 | if (src_chans * w * h != data.length) 195 | throw new ImageIOException("mismatching dimensions and length"); 196 | 197 | TGA_Encoder ec = { 198 | stream : stream, 199 | w : cast(ushort) w, 200 | h : cast(ushort) h, 201 | src_chans : cast(int) src_chans, 202 | tgt_chans : cast(int) ((tgt_chans) ? tgt_chans : src_chans), 203 | rle : true, 204 | data : data, 205 | }; 206 | 207 | write_tga(ec); 208 | stream.flush(); 209 | } 210 | 211 | struct TGA_Decoder { 212 | Reader stream; 213 | int w, h; 214 | bool origin_at_top; // src 215 | uint bytes_pp; 216 | bool rle; // run length compressed 217 | _ColFmt src_fmt; 218 | uint tgt_chans; 219 | } 220 | 221 | ubyte[] decode_tga(ref TGA_Decoder dc) { 222 | auto result = new ubyte[dc.w * dc.h * dc.tgt_chans]; 223 | 224 | const size_t tgt_linesize = dc.w * dc.tgt_chans; 225 | const size_t src_linesize = dc.w * dc.bytes_pp; 226 | auto src_line = new ubyte[src_linesize]; 227 | 228 | const ptrdiff_t tgt_stride = (dc.origin_at_top) ? tgt_linesize : -tgt_linesize; 229 | ptrdiff_t ti = (dc.origin_at_top) ? 0 : (dc.h-1) * tgt_linesize; 230 | 231 | const LineConv!ubyte convert = get_converter!ubyte(dc.src_fmt, dc.tgt_chans); 232 | 233 | if (!dc.rle) { 234 | foreach (_j; 0 .. dc.h) { 235 | dc.stream.readExact(src_line, src_linesize); 236 | convert(src_line, result[ti .. ti + tgt_linesize]); 237 | ti += tgt_stride; 238 | } 239 | return result; 240 | } 241 | 242 | // ----- RLE ----- 243 | 244 | ubyte[4] rbuf; 245 | size_t plen = 0; // packet length 246 | bool its_rle = false; 247 | 248 | foreach (_j; 0 .. dc.h) { 249 | // fill src_line with uncompressed data (this works like a stream) 250 | size_t wanted = src_linesize; 251 | while (wanted) { 252 | if (plen == 0) { 253 | dc.stream.readExact(rbuf, 1); 254 | its_rle = cast(bool) (rbuf[0] & 0x80); 255 | plen = ((rbuf[0] & 0x7f) + 1) * dc.bytes_pp; // length in bytes 256 | } 257 | const size_t gotten = src_linesize - wanted; 258 | const size_t copysize = min(plen, wanted); 259 | if (its_rle) { 260 | dc.stream.readExact(rbuf, dc.bytes_pp); 261 | for (size_t p = gotten; p < gotten+copysize; p += dc.bytes_pp) 262 | src_line[p .. p+dc.bytes_pp] = rbuf[0 .. dc.bytes_pp]; 263 | } else { // it's raw 264 | auto slice = src_line[gotten .. gotten+copysize]; 265 | dc.stream.readExact(slice, copysize); 266 | } 267 | wanted -= copysize; 268 | plen -= copysize; 269 | } 270 | 271 | convert(src_line, result[ti .. ti + tgt_linesize]); 272 | ti += tgt_stride; 273 | } 274 | 275 | return result; 276 | } 277 | 278 | // ---------------------------------------------------------------------- 279 | // TGA encoder 280 | 281 | immutable ubyte[18] tga_footer_sig = 282 | cast(immutable(ubyte)[18]) "TRUEVISION-XFILE.\0"; 283 | 284 | struct TGA_Encoder { 285 | Writer stream; 286 | ushort w, h; 287 | int src_chans; 288 | int tgt_chans; 289 | bool rle; // run length compression 290 | const(ubyte)[] data; 291 | } 292 | 293 | void write_tga(ref TGA_Encoder ec) { 294 | ubyte data_type; 295 | bool has_alpha = false; 296 | switch (ec.tgt_chans) with (TGA_DataType) { 297 | case 1: data_type = ec.rle ? Gray_RLE : Gray; break; 298 | case 2: data_type = ec.rle ? Gray_RLE : Gray; has_alpha = true; break; 299 | case 3: data_type = ec.rle ? TrueColor_RLE : TrueColor; break; 300 | case 4: data_type = ec.rle ? TrueColor_RLE : TrueColor; has_alpha = true; break; 301 | default: throw new ImageIOException("internal error"); 302 | } 303 | 304 | ubyte[18] hdr = void; 305 | hdr[0] = 0; // id length 306 | hdr[1] = 0; // palette type 307 | hdr[2] = data_type; 308 | hdr[3..8] = 0; // palette start (2), len (2), bits per palette entry (1) 309 | hdr[8..12] = 0; // x origin (2), y origin (2) 310 | hdr[12..14] = nativeToLittleEndian(ec.w); 311 | hdr[14..16] = nativeToLittleEndian(ec.h); 312 | hdr[16] = cast(ubyte) (ec.tgt_chans * 8); // bits per pixel 313 | hdr[17] = (has_alpha) ? 0x8 : 0x0; // flags: attr_bits_pp = 8 314 | ec.stream.rawWrite(hdr); 315 | 316 | write_image_data(ec); 317 | 318 | ubyte[26] ftr = void; 319 | ftr[0..4] = 0; // extension area offset 320 | ftr[4..8] = 0; // developer directory offset 321 | ftr[8..26] = tga_footer_sig; 322 | ec.stream.rawWrite(ftr); 323 | } 324 | 325 | void write_image_data(ref TGA_Encoder ec) { 326 | _ColFmt tgt_fmt; 327 | switch (ec.tgt_chans) { 328 | case 1: tgt_fmt = _ColFmt.Y; break; 329 | case 2: tgt_fmt = _ColFmt.YA; break; 330 | case 3: tgt_fmt = _ColFmt.BGR; break; 331 | case 4: tgt_fmt = _ColFmt.BGRA; break; 332 | default: throw new ImageIOException("internal error"); 333 | } 334 | 335 | const LineConv!ubyte convert = get_converter!ubyte(ec.src_chans, tgt_fmt); 336 | 337 | const size_t src_linesize = ec.w * ec.src_chans; 338 | const size_t tgt_linesize = ec.w * ec.tgt_chans; 339 | auto tgt_line = new ubyte[tgt_linesize]; 340 | 341 | ptrdiff_t si = (ec.h-1) * src_linesize; // origin at bottom 342 | 343 | if (!ec.rle) { 344 | foreach (_; 0 .. ec.h) { 345 | convert(ec.data[si .. si + src_linesize], tgt_line); 346 | ec.stream.rawWrite(tgt_line); 347 | si -= src_linesize; // origin at bottom 348 | } 349 | return; 350 | } 351 | 352 | // ----- RLE ----- 353 | 354 | const bytes_pp = ec.tgt_chans; 355 | const size_t max_packets_per_line = (tgt_linesize+127) / 128; 356 | auto tgt_cmp = new ubyte[tgt_linesize + max_packets_per_line]; // compressed line 357 | foreach (_; 0 .. ec.h) { 358 | convert(ec.data[si .. si + src_linesize], tgt_line); 359 | ubyte[] compressed_line = rle_compress(tgt_line, tgt_cmp, ec.w, bytes_pp); 360 | ec.stream.rawWrite(compressed_line); 361 | si -= src_linesize; // origin at bottom 362 | } 363 | } 364 | 365 | ubyte[] rle_compress(in ubyte[] line, ubyte[] tgt_cmp, in size_t w, in int bytes_pp) 366 | { 367 | const int rle_limit = (1 < bytes_pp) ? 2 : 3; // run len worth an RLE packet 368 | size_t runlen = 0; 369 | size_t rawlen = 0; 370 | size_t raw_i = 0; // start of raw packet data in line 371 | size_t cmp_i = 0; 372 | size_t pixels_left = w; 373 | const(ubyte)[] px; 374 | for (size_t i = bytes_pp; pixels_left; i += bytes_pp) { 375 | runlen = 1; 376 | px = line[i-bytes_pp .. i]; 377 | while (i < line.length && line[i .. i+bytes_pp] == px[0..$] && runlen < 128) { 378 | ++runlen; 379 | i += bytes_pp; 380 | } 381 | pixels_left -= runlen; 382 | 383 | if (runlen < rle_limit) { 384 | // data goes to raw packet 385 | rawlen += runlen; 386 | if (128 <= rawlen) { // full packet, need to store it 387 | size_t copysize = 128 * bytes_pp; 388 | tgt_cmp[cmp_i++] = 0x7f; // raw packet header 389 | tgt_cmp[cmp_i .. cmp_i+copysize] = line[raw_i .. raw_i+copysize]; 390 | cmp_i += copysize; 391 | raw_i += copysize; 392 | rawlen -= 128; 393 | } 394 | } else { 395 | // RLE packet is worth it 396 | 397 | // store raw packet first, if any 398 | if (rawlen) { 399 | assert(rawlen < 128); 400 | size_t copysize = rawlen * bytes_pp; 401 | tgt_cmp[cmp_i++] = cast(ubyte) (rawlen-1); // raw packet header 402 | tgt_cmp[cmp_i .. cmp_i+copysize] = line[raw_i .. raw_i+copysize]; 403 | cmp_i += copysize; 404 | rawlen = 0; 405 | } 406 | 407 | // store RLE packet 408 | tgt_cmp[cmp_i++] = cast(ubyte) (0x80 | (runlen-1)); // packet header 409 | tgt_cmp[cmp_i .. cmp_i+bytes_pp] = px[0..$]; // packet data 410 | cmp_i += bytes_pp; 411 | raw_i = i; 412 | } 413 | } // for 414 | 415 | if (rawlen) { // last packet of the line 416 | size_t copysize = rawlen * bytes_pp; 417 | tgt_cmp[cmp_i++] = cast(ubyte) (rawlen-1); // raw packet header 418 | tgt_cmp[cmp_i .. cmp_i+copysize] = line[raw_i .. raw_i+copysize]; 419 | cmp_i += copysize; 420 | } 421 | return tgt_cmp[0 .. cmp_i]; 422 | } 423 | 424 | enum TGA_DataType : ubyte { 425 | Idx = 1, 426 | TrueColor = 2, 427 | Gray = 3, 428 | Idx_RLE = 9, 429 | TrueColor_RLE = 10, 430 | Gray_RLE = 11, 431 | } 432 | 433 | package void read_tga_info(Reader stream, out int w, out int h, out int chans) { 434 | TGA_Header hdr = read_tga_header(stream); 435 | w = hdr.width; 436 | h = hdr.height; 437 | 438 | // TGA is awkward... 439 | auto dt = hdr.data_type; 440 | if ((dt == TGA_DataType.TrueColor || dt == TGA_DataType.Gray || 441 | dt == TGA_DataType.TrueColor_RLE || dt == TGA_DataType.Gray_RLE) 442 | && (hdr.bits_pp % 8) == 0) 443 | { 444 | chans = hdr.bits_pp / 8; 445 | return; 446 | } else if (dt == TGA_DataType.Idx || dt == TGA_DataType.Idx_RLE) { 447 | switch (hdr.palette_bits) { 448 | case 15: chans = 3; return; 449 | case 16: chans = 3; return; // one bit could be for some "interrupt control" 450 | case 24: chans = 3; return; 451 | case 32: chans = 4; return; 452 | default: 453 | } 454 | } 455 | chans = 0; // unknown 456 | } 457 | -------------------------------------------------------------------------------- /imageformats/bmp.d: -------------------------------------------------------------------------------- 1 | module imageformats.bmp; 2 | 3 | import std.bitmanip : littleEndianToNative, nativeToLittleEndian; 4 | import std.stdio : File, SEEK_SET; 5 | import std.math : abs; 6 | import std.typecons : scoped; 7 | import imageformats; 8 | 9 | private: 10 | 11 | immutable bmp_header = ['B', 'M']; 12 | 13 | /// Reads a BMP image. req_chans defines the format of returned image 14 | /// (you can use ColFmt here). 15 | public IFImage read_bmp(in char[] filename, long req_chans = 0) { 16 | auto reader = scoped!FileReader(filename); 17 | return read_bmp(reader, req_chans); 18 | } 19 | 20 | /// Reads an image from a buffer containing a BMP image. req_chans defines the 21 | /// format of returned image (you can use ColFmt here). 22 | public IFImage read_bmp_from_mem(in ubyte[] source, long req_chans = 0) { 23 | auto reader = scoped!MemReader(source); 24 | return read_bmp(reader, req_chans); 25 | } 26 | 27 | /// Returns the header of a BMP file. 28 | public BMP_Header read_bmp_header(in char[] filename) { 29 | auto reader = scoped!FileReader(filename); 30 | return read_bmp_header(reader); 31 | } 32 | 33 | /// Reads the image header from a buffer containing a BMP image. 34 | public BMP_Header read_bmp_header_from_mem(in ubyte[] source) { 35 | auto reader = scoped!MemReader(source); 36 | return read_bmp_header(reader); 37 | } 38 | 39 | /// Header of a BMP file. 40 | public struct BMP_Header { 41 | uint file_size; 42 | uint pixel_data_offset; 43 | 44 | uint dib_size; 45 | int width; 46 | int height; 47 | ushort planes; 48 | int bits_pp; 49 | uint dib_version; 50 | DibV1 dib_v1; 51 | DibV2 dib_v2; 52 | uint dib_v3_alpha_mask; 53 | DibV4 dib_v4; 54 | DibV5 dib_v5; 55 | } 56 | 57 | /// Part of BMP header, not always present. 58 | public struct DibV1 { 59 | uint compression; 60 | uint idat_size; 61 | uint pixels_per_meter_x; 62 | uint pixels_per_meter_y; 63 | uint palette_length; 64 | uint important_color_count; 65 | } 66 | 67 | /// Part of BMP header, not always present. 68 | public struct DibV2 { 69 | uint red_mask; 70 | uint green_mask; 71 | uint blue_mask; 72 | } 73 | 74 | /// Part of BMP header, not always present. 75 | public struct DibV4 { 76 | uint color_space_type; 77 | ubyte[36] color_space_endpoints; 78 | uint gamma_red; 79 | uint gamma_green; 80 | uint gamma_blue; 81 | } 82 | 83 | /// Part of BMP header, not always present. 84 | public struct DibV5 { 85 | uint icc_profile_data; 86 | uint icc_profile_size; 87 | } 88 | 89 | /// Returns width, height and color format information via w, h and chans. 90 | public void read_bmp_info(in char[] filename, out int w, out int h, out int chans) { 91 | auto reader = scoped!FileReader(filename); 92 | return read_bmp_info(reader, w, h, chans); 93 | } 94 | 95 | /// Returns width, height and color format information via w, h and chans. 96 | public void read_bmp_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) { 97 | auto reader = scoped!MemReader(source); 98 | return read_bmp_info(reader, w, h, chans); 99 | } 100 | 101 | /// Writes a BMP image into a file. 102 | public void write_bmp(in char[] file, long w, long h, in ubyte[] data, long tgt_chans = 0) 103 | { 104 | auto writer = scoped!FileWriter(file); 105 | write_bmp(writer, w, h, data, tgt_chans); 106 | } 107 | 108 | /// Writes a BMP image into a buffer. 109 | public ubyte[] write_bmp_to_mem(long w, long h, in ubyte[] data, long tgt_chans = 0) { 110 | auto writer = scoped!MemWriter(); 111 | write_bmp(writer, w, h, data, tgt_chans); 112 | return writer.result; 113 | } 114 | 115 | // Detects whether a BMP image is readable from stream. 116 | package bool detect_bmp(Reader stream) { 117 | try { 118 | ubyte[18] tmp = void; // bmp header + size of dib header 119 | stream.readExact(tmp, tmp.length); 120 | size_t ds = littleEndianToNative!uint(tmp[14..18]); 121 | return (tmp[0..2] == bmp_header 122 | && (ds == 12 || ds == 40 || ds == 52 || ds == 56 || ds == 108 || ds == 124)); 123 | } catch (Throwable) { 124 | return false; 125 | } finally { 126 | stream.seek(0, SEEK_SET); 127 | } 128 | } 129 | 130 | BMP_Header read_bmp_header(Reader stream) { 131 | ubyte[18] tmp = void; // bmp header + size of dib header 132 | stream.readExact(tmp[], tmp.length); 133 | 134 | if (tmp[0..2] != bmp_header) 135 | throw new ImageIOException("corrupt header"); 136 | 137 | uint dib_size = littleEndianToNative!uint(tmp[14..18]); 138 | uint dib_version; 139 | switch (dib_size) { 140 | case 12: dib_version = 0; break; 141 | case 40: dib_version = 1; break; 142 | case 52: dib_version = 2; break; 143 | case 56: dib_version = 3; break; 144 | case 108: dib_version = 4; break; 145 | case 124: dib_version = 5; break; 146 | default: throw new ImageIOException("unsupported dib version"); 147 | } 148 | auto dib_header = new ubyte[dib_size-4]; 149 | stream.readExact(dib_header[], dib_header.length); 150 | 151 | DibV1 dib_v1; 152 | DibV2 dib_v2; 153 | uint dib_v3_alpha_mask; 154 | DibV4 dib_v4; 155 | DibV5 dib_v5; 156 | 157 | if (1 <= dib_version) { 158 | DibV1 v1 = { 159 | compression : littleEndianToNative!uint(dib_header[12..16]), 160 | idat_size : littleEndianToNative!uint(dib_header[16..20]), 161 | pixels_per_meter_x : littleEndianToNative!uint(dib_header[20..24]), 162 | pixels_per_meter_y : littleEndianToNative!uint(dib_header[24..28]), 163 | palette_length : littleEndianToNative!uint(dib_header[28..32]), 164 | important_color_count : littleEndianToNative!uint(dib_header[32..36]), 165 | }; 166 | dib_v1 = v1; 167 | } 168 | 169 | if (2 <= dib_version) { 170 | DibV2 v2 = { 171 | red_mask : littleEndianToNative!uint(dib_header[36..40]), 172 | green_mask : littleEndianToNative!uint(dib_header[40..44]), 173 | blue_mask : littleEndianToNative!uint(dib_header[44..48]), 174 | }; 175 | dib_v2 = v2; 176 | } 177 | 178 | if (3 <= dib_version) { 179 | dib_v3_alpha_mask = littleEndianToNative!uint(dib_header[48..52]); 180 | } 181 | 182 | if (4 <= dib_version) { 183 | DibV4 v4 = { 184 | color_space_type : littleEndianToNative!uint(dib_header[52..56]), 185 | color_space_endpoints : dib_header[56..92], 186 | gamma_red : littleEndianToNative!uint(dib_header[92..96]), 187 | gamma_green : littleEndianToNative!uint(dib_header[96..100]), 188 | gamma_blue : littleEndianToNative!uint(dib_header[100..104]), 189 | }; 190 | dib_v4 = v4; 191 | } 192 | 193 | if (5 <= dib_version) { 194 | DibV5 v5 = { 195 | icc_profile_data : littleEndianToNative!uint(dib_header[108..112]), 196 | icc_profile_size : littleEndianToNative!uint(dib_header[112..116]), 197 | }; 198 | dib_v5 = v5; 199 | } 200 | 201 | int width, height; ushort planes; int bits_pp; 202 | if (0 == dib_version) { 203 | width = littleEndianToNative!ushort(dib_header[0..2]); 204 | height = littleEndianToNative!ushort(dib_header[2..4]); 205 | planes = littleEndianToNative!ushort(dib_header[4..6]); 206 | bits_pp = littleEndianToNative!ushort(dib_header[6..8]); 207 | } else { 208 | width = littleEndianToNative!int(dib_header[0..4]); 209 | height = littleEndianToNative!int(dib_header[4..8]); 210 | planes = littleEndianToNative!ushort(dib_header[8..10]); 211 | bits_pp = littleEndianToNative!ushort(dib_header[10..12]); 212 | } 213 | 214 | BMP_Header header = { 215 | file_size : littleEndianToNative!uint(tmp[2..6]), 216 | pixel_data_offset : littleEndianToNative!uint(tmp[10..14]), 217 | width : width, 218 | height : height, 219 | planes : planes, 220 | bits_pp : bits_pp, 221 | dib_version : dib_version, 222 | dib_v1 : dib_v1, 223 | dib_v2 : dib_v2, 224 | dib_v3_alpha_mask : dib_v3_alpha_mask, 225 | dib_v4 : dib_v4, 226 | dib_v5 : dib_v5, 227 | }; 228 | return header; 229 | } 230 | 231 | enum CMP_RGB = 0; 232 | enum CMP_BITS = 3; 233 | 234 | package IFImage read_bmp(Reader stream, long req_chans = 0) { 235 | if (req_chans < 0 || 4 < req_chans) 236 | throw new ImageIOException("unknown color format"); 237 | 238 | BMP_Header hdr = read_bmp_header(stream); 239 | 240 | if (hdr.width < 1 || hdr.height == 0) 241 | throw new ImageIOException("invalid dimensions"); 242 | if (hdr.pixel_data_offset < (14 + hdr.dib_size) 243 | || hdr.pixel_data_offset > 0xffffff /* arbitrary */) { 244 | throw new ImageIOException("invalid pixel data offset"); 245 | } 246 | if (hdr.planes != 1) 247 | throw new ImageIOException("not supported"); 248 | 249 | auto bytes_pp = 1; 250 | bool paletted = true; 251 | size_t palette_length = 256; 252 | bool rgb_masked = false; 253 | auto pe_bytes_pp = 3; 254 | 255 | if (1 <= hdr.dib_version) { 256 | if (256 < hdr.dib_v1.palette_length) 257 | throw new ImageIOException("ivnalid palette length"); 258 | if (hdr.bits_pp <= 8 && 259 | (hdr.dib_v1.palette_length == 0 || hdr.dib_v1.compression != CMP_RGB)) 260 | throw new ImageIOException("unsupported format"); 261 | if (hdr.dib_v1.compression != CMP_RGB && hdr.dib_v1.compression != CMP_BITS) 262 | throw new ImageIOException("unsupported compression"); 263 | 264 | switch (hdr.bits_pp) { 265 | case 8 : bytes_pp = 1; paletted = true; break; 266 | case 24 : bytes_pp = 3; paletted = false; break; 267 | case 32 : bytes_pp = 4; paletted = false; break; 268 | default: throw new ImageIOException("not supported"); 269 | } 270 | 271 | palette_length = hdr.dib_v1.palette_length; 272 | rgb_masked = hdr.dib_v1.compression == CMP_BITS; 273 | pe_bytes_pp = 4; 274 | } 275 | 276 | static size_t mask_to_idx(in uint mask) { 277 | switch (mask) { 278 | case 0xff00_0000: return 3; 279 | case 0x00ff_0000: return 2; 280 | case 0x0000_ff00: return 1; 281 | case 0x0000_00ff: return 0; 282 | default: throw new ImageIOException("unsupported mask"); 283 | } 284 | } 285 | 286 | size_t redi = 2; 287 | size_t greeni = 1; 288 | size_t bluei = 0; 289 | if (rgb_masked) { 290 | if (hdr.dib_version < 2) 291 | throw new ImageIOException("invalid format"); 292 | redi = mask_to_idx(hdr.dib_v2.red_mask); 293 | greeni = mask_to_idx(hdr.dib_v2.green_mask); 294 | bluei = mask_to_idx(hdr.dib_v2.blue_mask); 295 | } 296 | 297 | bool alpha_masked = false; 298 | size_t alphai = 0; 299 | if (bytes_pp == 4 && 3 <= hdr.dib_version && hdr.dib_v3_alpha_mask != 0) { 300 | alpha_masked = true; 301 | alphai = mask_to_idx(hdr.dib_v3_alpha_mask); 302 | } 303 | 304 | ubyte[] depaletted_line = null; 305 | ubyte[] palette = null; 306 | if (paletted) { 307 | depaletted_line = new ubyte[hdr.width * pe_bytes_pp]; 308 | palette = new ubyte[palette_length * pe_bytes_pp]; 309 | stream.readExact(palette[], palette.length); 310 | } 311 | 312 | stream.seek(hdr.pixel_data_offset, SEEK_SET); 313 | 314 | const tgt_chans = (0 < req_chans) ? req_chans 315 | : (alpha_masked) ? _ColFmt.RGBA 316 | : _ColFmt.RGB; 317 | 318 | const src_fmt = (!paletted || pe_bytes_pp == 4) ? _ColFmt.BGRA : _ColFmt.BGR; 319 | const LineConv!ubyte convert = get_converter!ubyte(src_fmt, tgt_chans); 320 | 321 | const size_t src_linesize = hdr.width * bytes_pp; // without padding 322 | const size_t src_pad = 3 - ((src_linesize-1) % 4); 323 | const ptrdiff_t tgt_linesize = (hdr.width * cast(int) tgt_chans); 324 | 325 | const ptrdiff_t tgt_stride = (hdr.height < 0) ? tgt_linesize : -tgt_linesize; 326 | ptrdiff_t ti = (hdr.height < 0) ? 0 : (hdr.height-1) * tgt_linesize; 327 | 328 | auto src_line = new ubyte[src_linesize + src_pad]; 329 | auto bgra_line_buf = (paletted) ? null : new ubyte[hdr.width * 4]; 330 | auto result = new ubyte[hdr.width * abs(hdr.height) * cast(int) tgt_chans]; 331 | 332 | foreach (_; 0 .. abs(hdr.height)) { 333 | stream.readExact(src_line[], src_line.length); 334 | 335 | if (paletted) { 336 | const size_t ps = pe_bytes_pp; 337 | size_t di = 0; 338 | foreach (idx; src_line[0..src_linesize]) { 339 | if (idx > palette_length) 340 | throw new ImageIOException("invalid palette index"); 341 | size_t i = idx * ps; 342 | depaletted_line[di .. di+ps] = palette[i .. i+ps]; 343 | if (ps == 4) { 344 | depaletted_line[di+3] = 255; 345 | } 346 | di += ps; 347 | } 348 | convert(depaletted_line[], result[ti .. ti + tgt_linesize]); 349 | } else { 350 | for (size_t si, di; si < src_linesize; si+=bytes_pp, di+=4) { 351 | bgra_line_buf[di + 0] = src_line[si + bluei]; 352 | bgra_line_buf[di + 1] = src_line[si + greeni]; 353 | bgra_line_buf[di + 2] = src_line[si + redi]; 354 | bgra_line_buf[di + 3] = (alpha_masked) ? src_line[si + alphai] 355 | : 255; 356 | } 357 | convert(bgra_line_buf[], result[ti .. ti + tgt_linesize]); 358 | } 359 | 360 | ti += tgt_stride; 361 | } 362 | 363 | IFImage ret = { 364 | w : hdr.width, 365 | h : abs(hdr.height), 366 | c : cast(ColFmt) tgt_chans, 367 | pixels : result, 368 | }; 369 | return ret; 370 | } 371 | 372 | package void read_bmp_info(Reader stream, out int w, out int h, out int chans) { 373 | BMP_Header hdr = read_bmp_header(stream); 374 | w = abs(hdr.width); 375 | h = abs(hdr.height); 376 | chans = (hdr.dib_version >= 3 && hdr.dib_v3_alpha_mask != 0 && hdr.bits_pp == 32) 377 | ? ColFmt.RGBA 378 | : ColFmt.RGB; 379 | } 380 | 381 | // ---------------------------------------------------------------------- 382 | // BMP encoder 383 | 384 | // Writes RGB or RGBA data. 385 | void write_bmp(Writer stream, long w, long h, in ubyte[] data, long tgt_chans = 0) { 386 | if (w < 1 || h < 1 || 0x7fff < w || 0x7fff < h) 387 | throw new ImageIOException("invalid dimensions"); 388 | size_t src_chans = data.length / cast(size_t) w / cast(size_t) h; 389 | if (src_chans < 1 || 4 < src_chans) 390 | throw new ImageIOException("invalid channel count"); 391 | if (tgt_chans != 0 && tgt_chans != 3 && tgt_chans != 4) 392 | throw new ImageIOException("unsupported format for writing"); 393 | if (src_chans * w * h != data.length) 394 | throw new ImageIOException("mismatching dimensions and length"); 395 | 396 | if (tgt_chans == 0) 397 | tgt_chans = (src_chans == 1 || src_chans == 3) ? 3 : 4; 398 | 399 | const dib_size = 108; 400 | const size_t tgt_linesize = cast(size_t) (w * tgt_chans); 401 | const size_t pad = 3 - ((tgt_linesize-1) & 3); 402 | const size_t idat_offset = 14 + dib_size; // bmp file header + dib header 403 | const size_t filesize = idat_offset + cast(size_t) h * (tgt_linesize + pad); 404 | if (filesize > 0xffff_ffff) { 405 | throw new ImageIOException("image too large"); 406 | } 407 | 408 | ubyte[14+dib_size] hdr; 409 | hdr[0] = 0x42; 410 | hdr[1] = 0x4d; 411 | hdr[2..6] = nativeToLittleEndian(cast(uint) filesize); 412 | hdr[6..10] = 0; // reserved 413 | hdr[10..14] = nativeToLittleEndian(cast(uint) idat_offset); // offset of pixel data 414 | hdr[14..18] = nativeToLittleEndian(cast(uint) dib_size); // dib header size 415 | hdr[18..22] = nativeToLittleEndian(cast(int) w); 416 | hdr[22..26] = nativeToLittleEndian(cast(int) h); // positive -> bottom-up 417 | hdr[26..28] = nativeToLittleEndian(cast(ushort) 1); // planes 418 | hdr[28..30] = nativeToLittleEndian(cast(ushort) (tgt_chans * 8)); // bits per pixel 419 | hdr[30..34] = nativeToLittleEndian((tgt_chans == 3) ? CMP_RGB : CMP_BITS); 420 | hdr[34..54] = 0; // rest of dib v1 421 | if (tgt_chans == 3) { 422 | hdr[54..70] = 0; // dib v2 and v3 423 | } else { 424 | static immutable ubyte[16] b = [ 425 | 0, 0, 0xff, 0, 426 | 0, 0xff, 0, 0, 427 | 0xff, 0, 0, 0, 428 | 0, 0, 0, 0xff 429 | ]; 430 | hdr[54..70] = b; 431 | } 432 | static immutable ubyte[4] BGRs = ['B', 'G', 'R', 's']; 433 | hdr[70..74] = BGRs; 434 | hdr[74..122] = 0; 435 | stream.rawWrite(hdr); 436 | 437 | const LineConv!ubyte convert = 438 | get_converter!ubyte(src_chans, (tgt_chans == 3) ? _ColFmt.BGR 439 | : _ColFmt.BGRA); 440 | 441 | auto tgt_line = new ubyte[tgt_linesize + pad]; 442 | const size_t src_linesize = cast(size_t) w * src_chans; 443 | size_t si = cast(size_t) h * src_linesize; 444 | 445 | foreach (_; 0..h) { 446 | si -= src_linesize; 447 | convert(data[si .. si + src_linesize], tgt_line[0..tgt_linesize]); 448 | stream.rawWrite(tgt_line); 449 | } 450 | 451 | stream.flush(); 452 | } 453 | -------------------------------------------------------------------------------- /imageformats/png.d: -------------------------------------------------------------------------------- 1 | module imageformats.png; 2 | 3 | import etc.c.zlib; 4 | import std.algorithm : min, reverse; 5 | import std.bitmanip : bigEndianToNative, nativeToBigEndian; 6 | import std.stdio : File, SEEK_SET; 7 | import std.digest.crc : CRC32, crc32Of; 8 | import std.typecons : scoped; 9 | import imageformats; 10 | 11 | private: 12 | 13 | /// Header of a PNG file. 14 | public struct PNG_Header { 15 | int width; 16 | int height; 17 | ubyte bit_depth; 18 | ubyte color_type; 19 | ubyte compression_method; 20 | ubyte filter_method; 21 | ubyte interlace_method; 22 | } 23 | 24 | /// Returns the header of a PNG file. 25 | public PNG_Header read_png_header(in char[] filename) { 26 | auto reader = scoped!FileReader(filename); 27 | return read_png_header(reader); 28 | } 29 | 30 | /// Returns the header of the image in the buffer. 31 | public PNG_Header read_png_header_from_mem(in ubyte[] source) { 32 | auto reader = scoped!MemReader(source); 33 | return read_png_header(reader); 34 | } 35 | 36 | /// Reads an 8-bit or 16-bit PNG image and returns it as an 8-bit image. 37 | /// req_chans defines the format of returned image (you can use ColFmt here). 38 | public IFImage read_png(in char[] filename, long req_chans = 0) { 39 | auto reader = scoped!FileReader(filename); 40 | return read_png(reader, req_chans); 41 | } 42 | 43 | /// Reads an 8-bit or 16-bit PNG image from a buffer and returns it as an 44 | /// 8-bit image. req_chans defines the format of returned image (you can use 45 | /// ColFmt here). 46 | public IFImage read_png_from_mem(in ubyte[] source, long req_chans = 0) { 47 | auto reader = scoped!MemReader(source); 48 | return read_png(reader, req_chans); 49 | } 50 | 51 | /// Reads an 8-bit or 16-bit PNG image and returns it as a 16-bit image. 52 | /// req_chans defines the format of returned image (you can use ColFmt here). 53 | public IFImage16 read_png16(in char[] filename, long req_chans = 0) { 54 | auto reader = scoped!FileReader(filename); 55 | return read_png16(reader, req_chans); 56 | } 57 | 58 | /// Reads an 8-bit or 16-bit PNG image from a buffer and returns it as a 59 | /// 16-bit image. req_chans defines the format of returned image (you can use 60 | /// ColFmt here). 61 | public IFImage16 read_png16_from_mem(in ubyte[] source, long req_chans = 0) { 62 | auto reader = scoped!MemReader(source); 63 | return read_png16(reader, req_chans); 64 | } 65 | 66 | /// Writes a PNG image into a file. 67 | public void write_png(in char[] file, long w, long h, in ubyte[] data, long tgt_chans = 0) 68 | { 69 | auto writer = scoped!FileWriter(file); 70 | write_png(writer, w, h, data, tgt_chans); 71 | } 72 | 73 | /// Writes a PNG image into a buffer. 74 | public ubyte[] write_png_to_mem(long w, long h, in ubyte[] data, long tgt_chans = 0) { 75 | auto writer = scoped!MemWriter(); 76 | write_png(writer, w, h, data, tgt_chans); 77 | return writer.result; 78 | } 79 | 80 | /// Returns width, height and color format information via w, h and chans. 81 | public void read_png_info(in char[] filename, out int w, out int h, out int chans) { 82 | auto reader = scoped!FileReader(filename); 83 | return read_png_info(reader, w, h, chans); 84 | } 85 | 86 | /// Returns width, height and color format information via w, h and chans. 87 | public void read_png_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) { 88 | auto reader = scoped!MemReader(source); 89 | return read_png_info(reader, w, h, chans); 90 | } 91 | 92 | // Detects whether a PNG image is readable from stream. 93 | package bool detect_png(Reader stream) { 94 | try { 95 | ubyte[8] tmp = void; 96 | stream.readExact(tmp, tmp.length); 97 | return (tmp[0..8] == png_file_header[0..$]); 98 | } catch (Throwable) { 99 | return false; 100 | } finally { 101 | stream.seek(0, SEEK_SET); 102 | } 103 | } 104 | 105 | PNG_Header read_png_header(Reader stream) { 106 | ubyte[33] tmp = void; // file header, IHDR len+type+data+crc 107 | stream.readExact(tmp, tmp.length); 108 | 109 | ubyte[4] crc = crc32Of(tmp[12..29]); 110 | reverse(crc[]); 111 | if ( tmp[0..8] != png_file_header[0..$] || 112 | tmp[8..16] != png_image_header || 113 | crc != tmp[29..33] ) 114 | throw new ImageIOException("corrupt header"); 115 | 116 | PNG_Header header = { 117 | width : bigEndianToNative!int(tmp[16..20]), 118 | height : bigEndianToNative!int(tmp[20..24]), 119 | bit_depth : tmp[24], 120 | color_type : tmp[25], 121 | compression_method : tmp[26], 122 | filter_method : tmp[27], 123 | interlace_method : tmp[28], 124 | }; 125 | return header; 126 | } 127 | 128 | package IFImage read_png(Reader stream, long req_chans = 0) { 129 | PNG_Decoder dc = init_png_decoder(stream, req_chans, 8); 130 | IFImage result = { 131 | w : dc.w, 132 | h : dc.h, 133 | c : cast(ColFmt) dc.tgt_chans, 134 | pixels : decode_png(dc).bpc8 135 | }; 136 | return result; 137 | } 138 | 139 | IFImage16 read_png16(Reader stream, long req_chans = 0) { 140 | PNG_Decoder dc = init_png_decoder(stream, req_chans, 16); 141 | IFImage16 result = { 142 | w : dc.w, 143 | h : dc.h, 144 | c : cast(ColFmt) dc.tgt_chans, 145 | pixels : decode_png(dc).bpc16 146 | }; 147 | return result; 148 | } 149 | 150 | PNG_Decoder init_png_decoder(Reader stream, long req_chans, int req_bpc) { 151 | if (req_chans < 0 || 4 < req_chans) 152 | throw new ImageIOException("come on..."); 153 | 154 | PNG_Header hdr = read_png_header(stream); 155 | 156 | if (hdr.width < 1 || hdr.height < 1 || int.max < cast(ulong) hdr.width * hdr.height) 157 | throw new ImageIOException("invalid dimensions"); 158 | if ((hdr.bit_depth != 8 && hdr.bit_depth != 16) || (req_bpc != 8 && req_bpc != 16)) 159 | throw new ImageIOException("only 8-bit and 16-bit images supported"); 160 | if (! (hdr.color_type == PNG_ColorType.Y || 161 | hdr.color_type == PNG_ColorType.RGB || 162 | hdr.color_type == PNG_ColorType.Idx || 163 | hdr.color_type == PNG_ColorType.YA || 164 | hdr.color_type == PNG_ColorType.RGBA) ) 165 | throw new ImageIOException("color type not supported"); 166 | if (hdr.compression_method != 0 || hdr.filter_method != 0 || 167 | (hdr.interlace_method != 0 && hdr.interlace_method != 1)) 168 | throw new ImageIOException("not supported"); 169 | 170 | PNG_Decoder dc = { 171 | stream : stream, 172 | src_indexed : (hdr.color_type == PNG_ColorType.Idx), 173 | src_chans : channels(cast(PNG_ColorType) hdr.color_type), 174 | bpc : hdr.bit_depth, 175 | req_bpc : req_bpc, 176 | ilace : hdr.interlace_method, 177 | w : hdr.width, 178 | h : hdr.height, 179 | }; 180 | dc.tgt_chans = (req_chans == 0) ? dc.src_chans : cast(int) req_chans; 181 | return dc; 182 | } 183 | 184 | immutable ubyte[8] png_file_header = 185 | [0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]; 186 | 187 | immutable ubyte[8] png_image_header = 188 | [0x0, 0x0, 0x0, 0xd, 'I','H','D','R']; 189 | 190 | int channels(PNG_ColorType ct) pure nothrow { 191 | final switch (ct) with (PNG_ColorType) { 192 | case Y: return 1; 193 | case RGB: return 3; 194 | case YA: return 2; 195 | case RGBA, Idx: return 4; 196 | } 197 | } 198 | 199 | PNG_ColorType color_type(long channels) pure nothrow { 200 | switch (channels) { 201 | case 1: return PNG_ColorType.Y; 202 | case 2: return PNG_ColorType.YA; 203 | case 3: return PNG_ColorType.RGB; 204 | case 4: return PNG_ColorType.RGBA; 205 | default: assert(0); 206 | } 207 | } 208 | 209 | struct PNG_Decoder { 210 | Reader stream; 211 | bool src_indexed; 212 | int src_chans; 213 | int tgt_chans; 214 | int bpc; 215 | int req_bpc; 216 | int w, h; 217 | ubyte ilace; 218 | 219 | CRC32 crc; 220 | ubyte[12] chunkmeta; // crc | length and type 221 | ubyte[] read_buf; 222 | ubyte[] palette; 223 | ubyte[] transparency; 224 | 225 | // decompression 226 | z_stream* z; // zlib stream 227 | uint avail_idat; // available bytes in current idat chunk 228 | ubyte[] idat_window; // slice of read_buf 229 | } 230 | 231 | Buffer decode_png(ref PNG_Decoder dc) { 232 | dc.read_buf = new ubyte[4096]; 233 | 234 | enum Stage { 235 | IHDR_parsed, 236 | PLTE_parsed, 237 | IDAT_parsed, 238 | IEND_parsed, 239 | } 240 | 241 | Buffer result; 242 | auto stage = Stage.IHDR_parsed; 243 | dc.stream.readExact(dc.chunkmeta[4..$], 8); // next chunk's len and type 244 | 245 | while (stage != Stage.IEND_parsed) { 246 | int len = bigEndianToNative!int(dc.chunkmeta[4..8]); 247 | if (len < 0) 248 | throw new ImageIOException("chunk too long"); 249 | 250 | // standard allows PLTE chunk for non-indexed images too but we don't 251 | dc.crc.put(dc.chunkmeta[8..12]); // type 252 | switch (cast(char[]) dc.chunkmeta[8..12]) { // chunk type 253 | case "IDAT": 254 | if (! (stage == Stage.IHDR_parsed || 255 | (stage == Stage.PLTE_parsed && dc.src_indexed)) ) 256 | throw new ImageIOException("corrupt chunk stream"); 257 | result = read_IDAT_stream(dc, len); 258 | dc.stream.readExact(dc.chunkmeta, 12); // crc | len, type 259 | ubyte[4] crc = dc.crc.finish; 260 | reverse(crc[]); 261 | if (crc != dc.chunkmeta[0..4]) 262 | throw new ImageIOException("corrupt chunk"); 263 | stage = Stage.IDAT_parsed; 264 | break; 265 | case "PLTE": 266 | if (stage != Stage.IHDR_parsed) 267 | throw new ImageIOException("corrupt chunk stream"); 268 | int entries = len / 3; 269 | if (len % 3 != 0 || 256 < entries) 270 | throw new ImageIOException("corrupt chunk"); 271 | dc.palette = new ubyte[len]; 272 | dc.stream.readExact(dc.palette, dc.palette.length); 273 | dc.crc.put(dc.palette); 274 | dc.stream.readExact(dc.chunkmeta, 12); // crc | len, type 275 | ubyte[4] crc = dc.crc.finish; 276 | reverse(crc[]); 277 | if (crc != dc.chunkmeta[0..4]) 278 | throw new ImageIOException("corrupt chunk"); 279 | stage = Stage.PLTE_parsed; 280 | break; 281 | case "tRNS": 282 | if (! (stage == Stage.IHDR_parsed || 283 | (stage == Stage.PLTE_parsed && dc.src_indexed)) ) 284 | throw new ImageIOException("corrupt chunk stream"); 285 | if (dc.src_indexed) { 286 | size_t entries = dc.palette.length / 3; 287 | if (len > entries) 288 | throw new ImageIOException("corrupt chunk"); 289 | } 290 | dc.transparency = new ubyte[len]; 291 | dc.stream.readExact(dc.transparency, dc.transparency.length); 292 | dc.stream.readExact(dc.chunkmeta, 12); 293 | dc.crc.put(dc.transparency); 294 | ubyte[4] crc = dc.crc.finish; 295 | reverse(crc[]); 296 | if (crc != dc.chunkmeta[0..4]) 297 | throw new ImageIOException("corrupt chunk"); 298 | break; 299 | case "IEND": 300 | if (stage != Stage.IDAT_parsed) 301 | throw new ImageIOException("corrupt chunk stream"); 302 | dc.stream.readExact(dc.chunkmeta, 4); // crc 303 | static immutable ubyte[4] expectedCRC = [0xae, 0x42, 0x60, 0x82]; 304 | if (len != 0 || dc.chunkmeta[0..4] != expectedCRC) 305 | throw new ImageIOException("corrupt chunk"); 306 | stage = Stage.IEND_parsed; 307 | break; 308 | case "IHDR": 309 | throw new ImageIOException("corrupt chunk stream"); 310 | default: 311 | // unknown chunk, ignore but check crc 312 | while (0 < len) { 313 | size_t bytes = min(len, dc.read_buf.length); 314 | dc.stream.readExact(dc.read_buf, bytes); 315 | len -= bytes; 316 | dc.crc.put(dc.read_buf[0..bytes]); 317 | } 318 | dc.stream.readExact(dc.chunkmeta, 12); // crc | len, type 319 | ubyte[4] crc = dc.crc.finish; 320 | reverse(crc[]); 321 | if (crc != dc.chunkmeta[0..4]) 322 | throw new ImageIOException("corrupt chunk"); 323 | } 324 | } 325 | 326 | return result; 327 | } 328 | 329 | enum PNG_ColorType : ubyte { 330 | Y = 0, 331 | RGB = 2, 332 | Idx = 3, 333 | YA = 4, 334 | RGBA = 6, 335 | } 336 | 337 | enum PNG_FilterType : ubyte { 338 | None = 0, 339 | Sub = 1, 340 | Up = 2, 341 | Average = 3, 342 | Paeth = 4, 343 | } 344 | 345 | enum InterlaceMethod { 346 | None = 0, Adam7 = 1 347 | } 348 | 349 | union Buffer { 350 | ubyte[] bpc8; 351 | ushort[] bpc16; 352 | } 353 | 354 | Buffer read_IDAT_stream(ref PNG_Decoder dc, int len) { 355 | assert(dc.req_bpc == 8 || dc.req_bpc == 16); 356 | 357 | // initialize zlib stream 358 | z_stream z = { zalloc: null, zfree: null, opaque: null }; 359 | if (inflateInit(&z) != Z_OK) 360 | throw new ImageIOException("can't init zlib"); 361 | dc.z = &z; 362 | dc.avail_idat = len; 363 | scope(exit) 364 | inflateEnd(&z); 365 | 366 | const size_t filter_step = dc.src_indexed 367 | ? 1 : dc.src_chans * (dc.bpc == 8 ? 1 : 2); 368 | 369 | ubyte[] depaletted = dc.src_indexed ? new ubyte[dc.w * 4] : null; 370 | 371 | auto cline = new ubyte[dc.w * filter_step + 1]; // +1 for filter type byte 372 | auto pline = new ubyte[dc.w * filter_step + 1]; // +1 for filter type byte 373 | auto cline8 = (dc.req_bpc == 8 && dc.bpc != 8) ? new ubyte[dc.w * dc.src_chans] : null; 374 | auto cline16 = (dc.req_bpc == 16) ? new ushort[dc.w * dc.src_chans] : null; 375 | ubyte[] result8 = (dc.req_bpc == 8) ? new ubyte[dc.w * dc.h * dc.tgt_chans] : null; 376 | ushort[] result16 = (dc.req_bpc == 16) ? new ushort[dc.w * dc.h * dc.tgt_chans] : null; 377 | 378 | const LineConv!ubyte convert8 = get_converter!ubyte(dc.src_chans, dc.tgt_chans); 379 | const LineConv!ushort convert16 = get_converter!ushort(dc.src_chans, dc.tgt_chans); 380 | 381 | if (dc.ilace == InterlaceMethod.None) { 382 | const size_t src_linelen = dc.w * dc.src_chans; 383 | const size_t tgt_linelen = dc.w * dc.tgt_chans; 384 | 385 | size_t ti = 0; // target index 386 | foreach (j; 0 .. dc.h) { 387 | uncompress(dc, cline); 388 | ubyte filter_type = cline[0]; 389 | 390 | recon(cline[1..$], pline[1..$], filter_type, filter_step); 391 | 392 | ubyte[] bytes; // defiltered bytes or 8-bit samples from palette 393 | if (dc.src_indexed) { 394 | depalette(dc.palette, dc.transparency, cline[1..$], depaletted); 395 | bytes = depaletted[0 .. src_linelen]; 396 | } else { 397 | bytes = cline[1..$]; 398 | } 399 | 400 | // convert colors 401 | if (dc.req_bpc == 8) { 402 | line8_from_bytes(bytes, dc.bpc, cline8); 403 | convert8(cline8[0 .. src_linelen], result8[ti .. ti + tgt_linelen]); 404 | } else { 405 | line16_from_bytes(bytes, dc.bpc, cline16); 406 | convert16(cline16[0 .. src_linelen], result16[ti .. ti + tgt_linelen]); 407 | } 408 | 409 | ti += tgt_linelen; 410 | 411 | ubyte[] _swap = pline; 412 | pline = cline; 413 | cline = _swap; 414 | } 415 | } else { 416 | // Adam7 interlacing 417 | 418 | immutable size_t[7] redw = [(dc.w + 7) / 8, 419 | (dc.w + 3) / 8, 420 | (dc.w + 3) / 4, 421 | (dc.w + 1) / 4, 422 | (dc.w + 1) / 2, 423 | (dc.w + 0) / 2, 424 | (dc.w + 0) / 1]; 425 | 426 | immutable size_t[7] redh = [(dc.h + 7) / 8, 427 | (dc.h + 7) / 8, 428 | (dc.h + 3) / 8, 429 | (dc.h + 3) / 4, 430 | (dc.h + 1) / 4, 431 | (dc.h + 1) / 2, 432 | (dc.h + 0) / 2]; 433 | 434 | auto redline8 = (dc.req_bpc == 8) ? new ubyte[dc.w * dc.tgt_chans] : null; 435 | auto redline16 = (dc.req_bpc == 16) ? new ushort[dc.w * dc.tgt_chans] : null; 436 | 437 | foreach (pass; 0 .. 7) { 438 | const A7_Catapult tgt_px = a7_catapults[pass]; // target pixel 439 | const size_t src_linelen = redw[pass] * dc.src_chans; 440 | ubyte[] cln = cline[0 .. redw[pass] * filter_step + 1]; 441 | ubyte[] pln = pline[0 .. redw[pass] * filter_step + 1]; 442 | pln[] = 0; 443 | 444 | foreach (j; 0 .. redh[pass]) { 445 | uncompress(dc, cln); 446 | ubyte filter_type = cln[0]; 447 | 448 | recon(cln[1..$], pln[1..$], filter_type, filter_step); 449 | 450 | ubyte[] bytes; // defiltered bytes or 8-bit samples from palette 451 | if (dc.src_indexed) { 452 | depalette(dc.palette, dc.transparency, cln[1..$], depaletted); 453 | bytes = depaletted[0 .. src_linelen]; 454 | } else { 455 | bytes = cln[1..$]; 456 | } 457 | 458 | // convert colors and sling pixels from reduced image to final buffer 459 | if (dc.req_bpc == 8) { 460 | line8_from_bytes(bytes, dc.bpc, cline8); 461 | convert8(cline8[0 .. src_linelen], redline8[0 .. redw[pass]*dc.tgt_chans]); 462 | for (size_t i, redi; i < redw[pass]; ++i, redi += dc.tgt_chans) { 463 | size_t tgt = tgt_px(i, j, dc.w) * dc.tgt_chans; 464 | result8[tgt .. tgt + dc.tgt_chans] = 465 | redline8[redi .. redi + dc.tgt_chans]; 466 | } 467 | } else { 468 | line16_from_bytes(bytes, dc.bpc, cline16); 469 | convert16(cline16[0 .. src_linelen], redline16[0 .. redw[pass]*dc.tgt_chans]); 470 | for (size_t i, redi; i < redw[pass]; ++i, redi += dc.tgt_chans) { 471 | size_t tgt = tgt_px(i, j, dc.w) * dc.tgt_chans; 472 | result16[tgt .. tgt + dc.tgt_chans] = 473 | redline16[redi .. redi + dc.tgt_chans]; 474 | } 475 | } 476 | 477 | ubyte[] _swap = pln; 478 | pln = cln; 479 | cln = _swap; 480 | } 481 | } 482 | } 483 | 484 | Buffer result; 485 | switch (dc.req_bpc) { 486 | case 8: result.bpc8 = result8; return result; 487 | case 16: result.bpc16 = result16; return result; 488 | default: throw new ImageIOException("internal error"); 489 | } 490 | } 491 | 492 | void line8_from_bytes(ubyte[] src, int bpc, ref ubyte[] tgt) { 493 | switch (bpc) { 494 | case 8: 495 | tgt = src; 496 | break; 497 | case 16: 498 | for (size_t k, t; k < src.length; k+=2, t+=1) { tgt[t] = src[k]; /* truncate */ } 499 | break; 500 | default: throw new ImageIOException("unsupported bit depth (and bug)"); 501 | } 502 | } 503 | 504 | void line16_from_bytes(in ubyte[] src, int bpc, ushort[] tgt) { 505 | switch (bpc) { 506 | case 8: 507 | for (size_t k; k < src.length; k+=1) { tgt[k] = src[k] * 256 + 128; } 508 | break; 509 | case 16: 510 | for (size_t k, t; k < src.length; k+=2, t+=1) { tgt[t] = src[k] << 8 | src[k+1]; } 511 | break; 512 | default: throw new ImageIOException("unsupported bit depth (and bug)"); 513 | } 514 | } 515 | 516 | void depalette(in ubyte[] palette, in ubyte[] transparency, in ubyte[] src_line, ubyte[] depaletted) pure { 517 | for (size_t s, d; s < src_line.length; s+=1, d+=4) { 518 | ubyte pid = src_line[s]; 519 | size_t pidx = pid * 3; 520 | if (palette.length < pidx + 3) 521 | throw new ImageIOException("palette index wrong"); 522 | depaletted[d .. d+3] = palette[pidx .. pidx+3]; 523 | depaletted[d+3] = (pid < transparency.length) ? transparency[pid] : 255; 524 | } 525 | } 526 | 527 | alias A7_Catapult = size_t function(size_t redx, size_t redy, size_t dstw); 528 | immutable A7_Catapult[7] a7_catapults = [ 529 | &a7_red1_to_dst, 530 | &a7_red2_to_dst, 531 | &a7_red3_to_dst, 532 | &a7_red4_to_dst, 533 | &a7_red5_to_dst, 534 | &a7_red6_to_dst, 535 | &a7_red7_to_dst, 536 | ]; 537 | 538 | pure nothrow { 539 | size_t a7_red1_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*8*dstw + redx*8; } 540 | size_t a7_red2_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*8*dstw + redx*8+4; } 541 | size_t a7_red3_to_dst(size_t redx, size_t redy, size_t dstw) { return (redy*8+4)*dstw + redx*4; } 542 | size_t a7_red4_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*4*dstw + redx*4+2; } 543 | size_t a7_red5_to_dst(size_t redx, size_t redy, size_t dstw) { return (redy*4+2)*dstw + redx*2; } 544 | size_t a7_red6_to_dst(size_t redx, size_t redy, size_t dstw) { return redy*2*dstw + redx*2+1; } 545 | size_t a7_red7_to_dst(size_t redx, size_t redy, size_t dstw) { return (redy*2+1)*dstw + redx; } 546 | } 547 | 548 | // Uncompresses a line from the IDAT stream into dst. 549 | void uncompress(ref PNG_Decoder dc, ubyte[] dst) 550 | { 551 | dc.z.avail_out = cast(uint) dst.length; 552 | dc.z.next_out = dst.ptr; 553 | 554 | while (true) { 555 | if (!dc.z.avail_in) { 556 | if (!dc.avail_idat) { 557 | dc.stream.readExact(dc.chunkmeta, 12); // crc | len & type 558 | ubyte[4] crc = dc.crc.finish; 559 | reverse(crc[]); 560 | if (crc != dc.chunkmeta[0..4]) 561 | throw new ImageIOException("corrupt chunk"); 562 | dc.avail_idat = bigEndianToNative!uint(dc.chunkmeta[4..8]); 563 | if (!dc.avail_idat) 564 | throw new ImageIOException("invalid data"); 565 | if (dc.chunkmeta[8..12] != "IDAT") 566 | throw new ImageIOException("not enough data"); 567 | dc.crc.put(dc.chunkmeta[8..12]); 568 | } 569 | 570 | const size_t n = min(dc.avail_idat, dc.read_buf.length); 571 | dc.stream.readExact(dc.read_buf, n); 572 | dc.idat_window = dc.read_buf[0..n]; 573 | 574 | if (!dc.idat_window) 575 | throw new ImageIOException("TODO"); 576 | dc.crc.put(dc.idat_window); 577 | dc.avail_idat -= cast(uint) dc.idat_window.length; 578 | dc.z.avail_in = cast(uint) dc.idat_window.length; 579 | dc.z.next_in = dc.idat_window.ptr; 580 | } 581 | 582 | int q = inflate(dc.z, Z_NO_FLUSH); 583 | 584 | if (dc.z.avail_out == 0) 585 | return; 586 | if (q != Z_OK) 587 | throw new ImageIOException("zlib error"); 588 | } 589 | } 590 | 591 | void recon(ubyte[] cline, in ubyte[] pline, ubyte ftype, size_t fstep) pure { 592 | switch (ftype) with (PNG_FilterType) { 593 | case None: 594 | break; 595 | case Sub: 596 | foreach (k; fstep .. cline.length) 597 | cline[k] += cline[k-fstep]; 598 | break; 599 | case Up: 600 | foreach (k; 0 .. cline.length) 601 | cline[k] += pline[k]; 602 | break; 603 | case Average: 604 | foreach (k; 0 .. fstep) 605 | cline[k] += pline[k] / 2; 606 | foreach (k; fstep .. cline.length) 607 | cline[k] += cast(ubyte) 608 | ((cast(uint) cline[k-fstep] + cast(uint) pline[k]) / 2); 609 | break; 610 | case Paeth: 611 | foreach (i; 0 .. fstep) 612 | cline[i] += paeth(0, pline[i], 0); 613 | foreach (i; fstep .. cline.length) 614 | cline[i] += paeth(cline[i-fstep], pline[i], pline[i-fstep]); 615 | break; 616 | default: 617 | throw new ImageIOException("filter type not supported"); 618 | } 619 | } 620 | 621 | ubyte paeth(ubyte a, ubyte b, ubyte c) pure nothrow { 622 | int pc = c; 623 | int pa = b - pc; 624 | int pb = a - pc; 625 | pc = pa + pb; 626 | if (pa < 0) pa = -pa; 627 | if (pb < 0) pb = -pb; 628 | if (pc < 0) pc = -pc; 629 | 630 | if (pa <= pb && pa <= pc) { 631 | return a; 632 | } else if (pb <= pc) { 633 | return b; 634 | } 635 | return c; 636 | } 637 | 638 | // ---------------------------------------------------------------------- 639 | // PNG encoder 640 | 641 | void write_png(Writer stream, long w, long h, in ubyte[] data, long tgt_chans = 0) { 642 | if (w < 1 || h < 1 || int.max < w || int.max < h) 643 | throw new ImageIOException("invalid dimensions"); 644 | uint src_chans = cast(uint) (data.length / w / h); 645 | if (src_chans < 1 || 4 < src_chans || tgt_chans < 0 || 4 < tgt_chans) 646 | throw new ImageIOException("invalid channel count"); 647 | if (src_chans * w * h != data.length) 648 | throw new ImageIOException("mismatching dimensions and length"); 649 | 650 | PNG_Encoder ec = { 651 | stream : stream, 652 | w : cast(size_t) w, 653 | h : cast(size_t) h, 654 | src_chans : src_chans, 655 | tgt_chans : tgt_chans ? cast(uint) tgt_chans : src_chans, 656 | data : data, 657 | }; 658 | 659 | write_png(ec); 660 | stream.flush(); 661 | } 662 | 663 | enum MAXIMUM_CHUNK_SIZE = 8192; 664 | 665 | struct PNG_Encoder { 666 | Writer stream; 667 | size_t w, h; 668 | uint src_chans; 669 | uint tgt_chans; 670 | const(ubyte)[] data; 671 | 672 | CRC32 crc; 673 | z_stream* z; 674 | ubyte[] idatbuf; 675 | } 676 | 677 | void write_png(ref PNG_Encoder ec) { 678 | ubyte[33] hdr = void; 679 | hdr[ 0 .. 8] = png_file_header; 680 | hdr[ 8 .. 16] = png_image_header; 681 | hdr[16 .. 20] = nativeToBigEndian(cast(uint) ec.w); 682 | hdr[20 .. 24] = nativeToBigEndian(cast(uint) ec.h); 683 | hdr[24 ] = 8; // bit depth 684 | hdr[25 ] = color_type(ec.tgt_chans); 685 | hdr[26 .. 29] = 0; // compression, filter and interlace methods 686 | ec.crc.start(); 687 | ec.crc.put(hdr[12 .. 29]); 688 | ubyte[4] crc = ec.crc.finish(); 689 | reverse(crc[]); 690 | hdr[29 .. 33] = crc; 691 | ec.stream.rawWrite(hdr); 692 | 693 | write_IDATs(ec); 694 | 695 | static immutable ubyte[12] iend = 696 | [0, 0, 0, 0, 'I','E','N','D', 0xae, 0x42, 0x60, 0x82]; 697 | ec.stream.rawWrite(iend); 698 | } 699 | 700 | void write_IDATs(ref PNG_Encoder ec) { 701 | // initialize zlib stream 702 | z_stream z = { zalloc: null, zfree: null, opaque: null }; 703 | if (deflateInit(&z, Z_DEFAULT_COMPRESSION) != Z_OK) 704 | throw new ImageIOException("zlib init error"); 705 | scope(exit) 706 | deflateEnd(ec.z); 707 | ec.z = &z; 708 | 709 | const LineConv!ubyte convert = get_converter!ubyte(ec.src_chans, ec.tgt_chans); 710 | 711 | const size_t filter_step = ec.tgt_chans; // step between pixels, in bytes 712 | const size_t slinesz = ec.w * ec.src_chans; 713 | const size_t tlinesz = ec.w * ec.tgt_chans + 1; 714 | const size_t workbufsz = 3 * tlinesz + MAXIMUM_CHUNK_SIZE; 715 | 716 | ubyte[] workbuf = new ubyte[workbufsz]; 717 | ubyte[] cline = workbuf[0 .. tlinesz]; 718 | ubyte[] pline = workbuf[tlinesz .. 2 * tlinesz]; 719 | ubyte[] filtered = workbuf[2 * tlinesz .. 3 * tlinesz]; 720 | ec.idatbuf = workbuf[$-MAXIMUM_CHUNK_SIZE .. $]; 721 | workbuf[0..$] = 0; 722 | ec.z.avail_out = cast(uint) ec.idatbuf.length; 723 | ec.z.next_out = ec.idatbuf.ptr; 724 | 725 | const size_t ssize = ec.w * ec.src_chans * ec.h; 726 | 727 | for (size_t si; si < ssize; si += slinesz) { 728 | convert(ec.data[si .. si + slinesz], cline[1..$]); 729 | 730 | // these loops could be merged with some extra space... 731 | foreach (i; 1 .. filter_step+1) 732 | filtered[i] = cast(ubyte) (cline[i] - paeth(0, pline[i], 0)); 733 | foreach (i; filter_step+1 .. tlinesz) 734 | filtered[i] = cast(ubyte) 735 | (cline[i] - paeth(cline[i-filter_step], pline[i], pline[i-filter_step])); 736 | filtered[0] = PNG_FilterType.Paeth; 737 | 738 | compress(ec, filtered); 739 | ubyte[] _swap = pline; 740 | pline = cline; 741 | cline = _swap; 742 | } 743 | 744 | while (true) { // flush zlib 745 | int q = deflate(ec.z, Z_FINISH); 746 | if (ec.idatbuf.length - ec.z.avail_out > 0) 747 | flush_idat(ec); 748 | if (q == Z_STREAM_END) break; 749 | if (q == Z_OK) continue; // not enough avail_out 750 | throw new ImageIOException("zlib compression error"); 751 | } 752 | } 753 | 754 | void compress(ref PNG_Encoder ec, in ubyte[] line) 755 | { 756 | ec.z.avail_in = cast(uint) line.length; 757 | ec.z.next_in = line.ptr; 758 | while (ec.z.avail_in) { 759 | int q = deflate(ec.z, Z_NO_FLUSH); 760 | if (q != Z_OK) 761 | throw new ImageIOException("zlib compression error"); 762 | if (ec.z.avail_out == 0) 763 | flush_idat(ec); 764 | } 765 | } 766 | 767 | void flush_idat(ref PNG_Encoder ec) // writes an idat chunk 768 | { 769 | const uint len = cast(uint) (ec.idatbuf.length - ec.z.avail_out); 770 | ec.crc.put(cast(const(ubyte)[]) "IDAT"); 771 | ec.crc.put(ec.idatbuf[0 .. len]); 772 | ubyte[8] meta; 773 | meta[0..4] = nativeToBigEndian!uint(len); 774 | meta[4..8] = cast(ubyte[4]) "IDAT"; 775 | ec.stream.rawWrite(meta); 776 | ec.stream.rawWrite(ec.idatbuf[0 .. len]); 777 | ubyte[4] crc = ec.crc.finish(); 778 | reverse(crc[]); 779 | ec.stream.rawWrite(crc[0..$]); 780 | ec.z.next_out = ec.idatbuf.ptr; 781 | ec.z.avail_out = cast(uint) ec.idatbuf.length; 782 | } 783 | 784 | package void read_png_info(Reader stream, out int w, out int h, out int chans) { 785 | PNG_Header hdr = read_png_header(stream); 786 | w = hdr.width; 787 | h = hdr.height; 788 | chans = channels(cast(PNG_ColorType) hdr.color_type); 789 | } 790 | -------------------------------------------------------------------------------- /imageformats/jpeg.d: -------------------------------------------------------------------------------- 1 | // Baseline JPEG decoder 2 | 3 | module imageformats.jpeg; 4 | 5 | import std.math : ceil; 6 | import std.bitmanip : bigEndianToNative; 7 | import std.stdio : File, SEEK_SET, SEEK_CUR; 8 | import std.typecons : scoped; 9 | import imageformats; 10 | 11 | private: 12 | 13 | /// Reads a JPEG image. req_chans defines the format of returned image 14 | /// (you can use ColFmt here). 15 | public IFImage read_jpeg(in char[] filename, long req_chans = 0) { 16 | auto reader = scoped!FileReader(filename); 17 | return read_jpeg(reader, req_chans); 18 | } 19 | 20 | /// Reads an image from a buffer containing a JPEG image. req_chans defines the 21 | /// format of returned image (you can use ColFmt here). 22 | public IFImage read_jpeg_from_mem(in ubyte[] source, long req_chans = 0) { 23 | auto reader = scoped!MemReader(source); 24 | return read_jpeg(reader, req_chans); 25 | } 26 | 27 | /// Returns width, height and color format information via w, h and chans. 28 | public void read_jpeg_info(in char[] filename, out int w, out int h, out int chans) { 29 | auto reader = scoped!FileReader(filename); 30 | return read_jpeg_info(reader, w, h, chans); 31 | } 32 | 33 | /// Returns width, height and color format information via w, h and chans. 34 | public void read_jpeg_info_from_mem(in ubyte[] source, out int w, out int h, out int chans) { 35 | auto reader = scoped!MemReader(source); 36 | return read_jpeg_info(reader, w, h, chans); 37 | } 38 | 39 | // Detects whether a JPEG image is readable from stream. 40 | package bool detect_jpeg(Reader stream) { 41 | try { 42 | int w, h, c; 43 | read_jpeg_info(stream, w, h, c); 44 | return true; 45 | } catch (Throwable) { 46 | return false; 47 | } finally { 48 | stream.seek(0, SEEK_SET); 49 | } 50 | } 51 | 52 | package void read_jpeg_info(Reader stream, out int w, out int h, out int chans) { 53 | ubyte[2] marker = void; 54 | stream.readExact(marker, 2); 55 | 56 | // SOI 57 | if (marker[0] != 0xff || marker[1] != Marker.SOI) 58 | throw new ImageIOException("not JPEG"); 59 | 60 | while (true) { 61 | stream.readExact(marker, 2); 62 | 63 | if (marker[0] != 0xff) 64 | throw new ImageIOException("no frame header"); 65 | while (marker[1] == 0xff) 66 | stream.readExact(marker[1..$], 1); 67 | 68 | switch (marker[1]) with (Marker) { 69 | case SOF0: .. case SOF3: 70 | case SOF9: .. case SOF11: 71 | ubyte[8] tmp; 72 | stream.readExact(tmp[0..8], 8); 73 | //int len = bigEndianToNative!ushort(tmp[0..2]); 74 | w = bigEndianToNative!ushort(tmp[5..7]); 75 | h = bigEndianToNative!ushort(tmp[3..5]); 76 | chans = tmp[7]; 77 | return; 78 | case SOS, EOI: throw new ImageIOException("no frame header"); 79 | case DRI, DHT, DQT, COM: 80 | case APP0: .. case APPf: 81 | ubyte[2] lenbuf = void; 82 | stream.readExact(lenbuf, 2); 83 | int skiplen = bigEndianToNative!ushort(lenbuf) - 2; 84 | stream.seek(skiplen, SEEK_CUR); 85 | break; 86 | default: throw new ImageIOException("unsupported marker"); 87 | } 88 | } 89 | assert(0); 90 | } 91 | 92 | package IFImage read_jpeg(Reader stream, long req_chans = 0) { 93 | if (req_chans < 0 || 4 < req_chans) 94 | throw new ImageIOException("come on..."); 95 | 96 | // SOI 97 | ubyte[2] tmp = void; 98 | stream.readExact(tmp, tmp.length); 99 | if (tmp[0] != 0xff || tmp[1] != Marker.SOI) 100 | throw new ImageIOException("not JPEG"); 101 | 102 | JPEG_Decoder dc = { stream: stream }; 103 | 104 | read_markers(dc); // reads until first scan header or eoi 105 | if (dc.eoi_reached) 106 | throw new ImageIOException("no image data"); 107 | 108 | dc.tgt_chans = (req_chans == 0) ? dc.num_comps : cast(int) req_chans; 109 | 110 | foreach (ref comp; dc.comps[0..dc.num_comps]) 111 | comp.data = new ubyte[dc.num_mcu_x*comp.sfx*8*dc.num_mcu_y*comp.sfy*8]; 112 | 113 | // E.7 -- Multiple scans are for progressive images which are not supported 114 | //while (!dc.eoi_reached) { 115 | decode_scan(dc); // E.2.3 116 | //read_markers(dc); // reads until next scan header or eoi 117 | //} 118 | 119 | // throw away fill samples and convert to target format 120 | ubyte[] pixels = dc.reconstruct(); 121 | 122 | IFImage result = { 123 | w : dc.width, 124 | h : dc.height, 125 | c : cast(ColFmt) dc.tgt_chans, 126 | pixels : pixels, 127 | }; 128 | return result; 129 | } 130 | 131 | struct JPEG_Decoder { 132 | Reader stream; 133 | 134 | bool has_frame_header = false; 135 | bool eoi_reached = false; 136 | 137 | ubyte[64][4] qtables; 138 | HuffTab[2] ac_tables; 139 | HuffTab[2] dc_tables; 140 | 141 | ubyte cb; // current byte (next bit always at MSB) 142 | int bits_left; // num of unused bits in cb 143 | 144 | bool correct_comp_ids; 145 | Component[3] comps; 146 | ubyte num_comps; 147 | int tgt_chans; 148 | 149 | int width, height; 150 | 151 | int hmax, vmax; 152 | 153 | ushort restart_interval; // number of MCUs in restart interval 154 | 155 | // image component 156 | struct Component { 157 | ubyte sfx, sfy; // sampling factors, aka. h and v 158 | size_t x, y; // total num of samples, without fill samples 159 | ubyte qtable; 160 | ubyte ac_table; 161 | ubyte dc_table; 162 | int pred; // dc prediction 163 | ubyte[] data; // reconstructed samples 164 | } 165 | 166 | int num_mcu_x; 167 | int num_mcu_y; 168 | } 169 | 170 | struct HuffTab { 171 | ubyte[256] values; 172 | ubyte[257] sizes; 173 | short[16] mincode, maxcode; 174 | short[16] valptr; 175 | } 176 | 177 | enum Marker : ubyte { 178 | SOI = 0xd8, // start of image 179 | SOF0 = 0xc0, // start of frame / baseline DCT 180 | //SOF1 = 0xc1, // start of frame / extended seq. 181 | //SOF2 = 0xc2, // start of frame / progressive DCT 182 | SOF3 = 0xc3, // start of frame / lossless 183 | SOF9 = 0xc9, // start of frame / extended seq., arithmetic 184 | SOF11 = 0xcb, // start of frame / lossless, arithmetic 185 | DHT = 0xc4, // define huffman tables 186 | DQT = 0xdb, // define quantization tables 187 | DRI = 0xdd, // define restart interval 188 | SOS = 0xda, // start of scan 189 | DNL = 0xdc, // define number of lines 190 | RST0 = 0xd0, // restart entropy coded data 191 | // ... 192 | RST7 = 0xd7, // restart entropy coded data 193 | APP0 = 0xe0, // application 0 segment 194 | // ... 195 | APPf = 0xef, // application f segment 196 | //DAC = 0xcc, // define arithmetic conditioning table 197 | COM = 0xfe, // comment 198 | EOI = 0xd9, // end of image 199 | } 200 | 201 | void read_markers(ref JPEG_Decoder dc) { 202 | bool has_next_scan_header = false; 203 | while (!has_next_scan_header && !dc.eoi_reached) { 204 | ubyte[2] marker; 205 | dc.stream.readExact(marker, 2); 206 | 207 | if (marker[0] != 0xff) 208 | throw new ImageIOException("no marker"); 209 | while (marker[1] == 0xff) 210 | dc.stream.readExact(marker[1..$], 1); 211 | 212 | switch (marker[1]) with (Marker) { 213 | case DHT: dc.read_huffman_tables(); break; 214 | case DQT: dc.read_quantization_tables(); break; 215 | case SOF0: 216 | if (dc.has_frame_header) 217 | throw new ImageIOException("extra frame header"); 218 | dc.read_frame_header(); 219 | dc.has_frame_header = true; 220 | break; 221 | case SOS: 222 | if (!dc.has_frame_header) 223 | throw new ImageIOException("no frame header"); 224 | dc.read_scan_header(); 225 | has_next_scan_header = true; 226 | break; 227 | case DRI: dc.read_restart_interval(); break; 228 | case EOI: dc.eoi_reached = true; break; 229 | case APP0: .. case APPf: 230 | case COM: 231 | ubyte[2] lenbuf = void; 232 | dc.stream.readExact(lenbuf, lenbuf.length); 233 | int len = bigEndianToNative!ushort(lenbuf) - 2; 234 | dc.stream.seek(len, SEEK_CUR); 235 | break; 236 | default: throw new ImageIOException("invalid / unsupported marker"); 237 | } 238 | } 239 | } 240 | 241 | // DHT -- define huffman tables 242 | void read_huffman_tables(ref JPEG_Decoder dc) { 243 | ubyte[19] tmp = void; 244 | dc.stream.readExact(tmp, 2); 245 | int len = bigEndianToNative!ushort(tmp[0..2]); 246 | len -= 2; 247 | 248 | while (0 < len) { 249 | dc.stream.readExact(tmp, 17); // info byte & the BITS 250 | ubyte table_slot = tmp[0] & 0xf; // must be 0 or 1 for baseline 251 | ubyte table_class = tmp[0] >> 4; // 0 = dc table, 1 = ac table 252 | if (1 < table_slot || 1 < table_class) 253 | throw new ImageIOException("invalid / not supported"); 254 | 255 | // compute total number of huffman codes 256 | int mt = 0; 257 | foreach (i; 1..17) 258 | mt += tmp[i]; 259 | if (256 < mt) // TODO where in the spec? 260 | throw new ImageIOException("invalid / not supported"); 261 | 262 | if (table_class == 0) { 263 | dc.stream.readExact(dc.dc_tables[table_slot].values, mt); 264 | derive_table(dc.dc_tables[table_slot], tmp[1..17]); 265 | } else { 266 | dc.stream.readExact(dc.ac_tables[table_slot].values, mt); 267 | derive_table(dc.ac_tables[table_slot], tmp[1..17]); 268 | } 269 | 270 | len -= 17 + mt; 271 | } 272 | } 273 | 274 | // num_values is the BITS 275 | void derive_table(ref HuffTab table, in ref ubyte[16] num_values) { 276 | short[256] codes; 277 | 278 | int k = 0; 279 | foreach (i; 0..16) { 280 | foreach (j; 0..num_values[i]) { 281 | table.sizes[k] = cast(ubyte) (i + 1); 282 | ++k; 283 | } 284 | } 285 | table.sizes[k] = 0; 286 | 287 | k = 0; 288 | short code = 0; 289 | ubyte si = table.sizes[k]; 290 | while (true) { 291 | do { 292 | codes[k] = code; 293 | ++code; 294 | ++k; 295 | } while (si == table.sizes[k]); 296 | 297 | if (table.sizes[k] == 0) 298 | break; 299 | 300 | // assert(si < table.sizes[k]); 301 | do { 302 | code <<= 1; 303 | ++si; 304 | } while (si != table.sizes[k]); 305 | } 306 | 307 | derive_mincode_maxcode_valptr( 308 | table.mincode, table.maxcode, table.valptr, 309 | codes, num_values 310 | ); 311 | } 312 | 313 | // F.15 314 | void derive_mincode_maxcode_valptr(ref short[16] mincode, ref short[16] maxcode, 315 | ref short[16] valptr, in ref short[256] codes, in ref ubyte[16] num_values) pure 316 | { 317 | mincode[] = -1; 318 | maxcode[] = -1; 319 | valptr[] = -1; 320 | 321 | int j = 0; 322 | foreach (i; 0..16) { 323 | if (num_values[i] != 0) { 324 | valptr[i] = cast(short) j; 325 | mincode[i] = codes[j]; 326 | j += num_values[i] - 1; 327 | maxcode[i] = codes[j]; 328 | j += 1; 329 | } 330 | } 331 | } 332 | 333 | // DQT -- define quantization tables 334 | void read_quantization_tables(ref JPEG_Decoder dc) { 335 | ubyte[2] tmp = void; 336 | dc.stream.readExact(tmp, 2); 337 | int len = bigEndianToNative!ushort(tmp[0..2]); 338 | if (len % 65 != 2) 339 | throw new ImageIOException("invalid / not supported"); 340 | len -= 2; 341 | while (0 < len) { 342 | dc.stream.readExact(tmp, 1); 343 | ubyte table_info = tmp[0]; 344 | ubyte table_slot = table_info & 0xf; 345 | ubyte precision = table_info >> 4; // 0 = 8 bit, 1 = 16 bit 346 | if (3 < table_slot || precision != 0) // only 8 bit for baseline 347 | throw new ImageIOException("invalid / not supported"); 348 | 349 | dc.stream.readExact(dc.qtables[table_slot], 64); 350 | len -= 1 + 64; 351 | } 352 | } 353 | 354 | // SOF0 -- start of frame 355 | void read_frame_header(ref JPEG_Decoder dc) { 356 | ubyte[9] tmp = void; 357 | dc.stream.readExact(tmp, 8); 358 | int len = bigEndianToNative!ushort(tmp[0..2]); // 8 + num_comps*3 359 | ubyte precision = tmp[2]; 360 | dc.height = bigEndianToNative!ushort(tmp[3..5]); 361 | dc.width = bigEndianToNative!ushort(tmp[5..7]); 362 | dc.num_comps = tmp[7]; 363 | 364 | if ( precision != 8 || 365 | (dc.num_comps != 1 && dc.num_comps != 3) || 366 | len != 8 + dc.num_comps*3 ) 367 | throw new ImageIOException("invalid / not supported"); 368 | 369 | dc.hmax = 0; 370 | dc.vmax = 0; 371 | int mcu_du = 0; // data units in one mcu 372 | dc.stream.readExact(tmp, dc.num_comps*3); 373 | foreach (i; 0..dc.num_comps) { 374 | ubyte ci = tmp[i*3]; 375 | // JFIF says ci should be i+1, but there are images where ci is i. Normalize ids 376 | // so that ci == i, always. So much for standards... 377 | if (i == 0) { dc.correct_comp_ids = ci == i+1; } 378 | if ((dc.correct_comp_ids && ci != i+1) 379 | || (!dc.correct_comp_ids && ci != i)) 380 | throw new ImageIOException("invalid component id"); 381 | 382 | auto comp = &dc.comps[i]; 383 | ubyte sampling_factors = tmp[i*3 + 1]; 384 | comp.sfx = sampling_factors >> 4; 385 | comp.sfy = sampling_factors & 0xf; 386 | comp.qtable = tmp[i*3 + 2]; 387 | if ( comp.sfy < 1 || 4 < comp.sfy || 388 | comp.sfx < 1 || 4 < comp.sfx || 389 | 3 < comp.qtable ) 390 | throw new ImageIOException("invalid / not supported"); 391 | 392 | if (dc.hmax < comp.sfx) dc.hmax = comp.sfx; 393 | if (dc.vmax < comp.sfy) dc.vmax = comp.sfy; 394 | 395 | mcu_du += comp.sfx * comp.sfy; 396 | } 397 | if (10 < mcu_du) 398 | throw new ImageIOException("invalid / not supported"); 399 | 400 | foreach (i; 0..dc.num_comps) { 401 | dc.comps[i].x = cast(size_t) ceil(dc.width * (cast(double) dc.comps[i].sfx / dc.hmax)); 402 | dc.comps[i].y = cast(size_t) ceil(dc.height * (cast(double) dc.comps[i].sfy / dc.vmax)); 403 | } 404 | 405 | size_t mcu_w = dc.hmax * 8; 406 | size_t mcu_h = dc.vmax * 8; 407 | dc.num_mcu_x = cast(int) ((dc.width + mcu_w-1) / mcu_w); 408 | dc.num_mcu_y = cast(int) ((dc.height + mcu_h-1) / mcu_h); 409 | 410 | debug(DebugJPEG) { 411 | writefln("\tlen: %s", len); 412 | writefln("\tprecision: %s", precision); 413 | writefln("\tdimensions: %s x %s", dc.width, dc.height); 414 | writefln("\tnum_comps: %s", dc.num_comps); 415 | writefln("\tnum_mcu_x: %s", dc.num_mcu_x); 416 | writefln("\tnum_mcu_y: %s", dc.num_mcu_y); 417 | } 418 | 419 | } 420 | 421 | // SOS -- start of scan 422 | void read_scan_header(ref JPEG_Decoder dc) { 423 | ubyte[3] tmp = void; 424 | dc.stream.readExact(tmp, tmp.length); 425 | ushort len = bigEndianToNative!ushort(tmp[0..2]); 426 | ubyte num_scan_comps = tmp[2]; 427 | 428 | if ( num_scan_comps != dc.num_comps || 429 | len != (6+num_scan_comps*2) ) 430 | throw new ImageIOException("invalid / not supported"); 431 | 432 | ubyte[16] buf; 433 | dc.stream.readExact(buf, len-3); 434 | 435 | foreach (i; 0..num_scan_comps) { 436 | uint ci = buf[i*2] - ((dc.correct_comp_ids) ? 1 : 0); 437 | if (ci >= dc.num_comps) 438 | throw new ImageIOException("invalid component id"); 439 | 440 | ubyte tables = buf[i*2+1]; 441 | dc.comps[ci].dc_table = tables >> 4; 442 | dc.comps[ci].ac_table = tables & 0xf; 443 | if ( 1 < dc.comps[ci].dc_table || 444 | 1 < dc.comps[ci].ac_table ) 445 | throw new ImageIOException("invalid / not supported"); 446 | } 447 | 448 | // ignore these 449 | //ubyte spectral_start = buf[$-3]; 450 | //ubyte spectral_end = buf[$-2]; 451 | //ubyte approx = buf[$-1]; 452 | } 453 | 454 | void read_restart_interval(ref JPEG_Decoder dc) { 455 | ubyte[4] tmp = void; 456 | dc.stream.readExact(tmp, tmp.length); 457 | ushort len = bigEndianToNative!ushort(tmp[0..2]); 458 | if (len != 4) 459 | throw new ImageIOException("invalid / not supported"); 460 | dc.restart_interval = bigEndianToNative!ushort(tmp[2..4]); 461 | } 462 | 463 | // E.2.3 and E.8 and E.9 464 | void decode_scan(ref JPEG_Decoder dc) { 465 | int intervals, mcus; 466 | if (0 < dc.restart_interval) { 467 | int total_mcus = dc.num_mcu_x * dc.num_mcu_y; 468 | intervals = (total_mcus + dc.restart_interval-1) / dc.restart_interval; 469 | mcus = dc.restart_interval; 470 | } else { 471 | intervals = 1; 472 | mcus = dc.num_mcu_x * dc.num_mcu_y; 473 | } 474 | 475 | foreach (mcu_j; 0 .. dc.num_mcu_y) { 476 | foreach (mcu_i; 0 .. dc.num_mcu_x) { 477 | 478 | // decode mcu 479 | foreach (c; 0..dc.num_comps) { 480 | auto comp = &dc.comps[c]; 481 | foreach (du_j; 0 .. comp.sfy) { 482 | foreach (du_i; 0 .. comp.sfx) { 483 | // decode entropy, dequantize & dezigzag 484 | short[64] data = decode_block(dc, *comp, dc.qtables[comp.qtable]); 485 | // idct & level-shift 486 | int outx = (mcu_i * comp.sfx + du_i) * 8; 487 | int outy = (mcu_j * comp.sfy + du_j) * 8; 488 | int dst_stride = dc.num_mcu_x * comp.sfx*8; 489 | ubyte* dst = comp.data.ptr + outy*dst_stride + outx; 490 | stbi__idct_block(dst, dst_stride, data); 491 | } 492 | } 493 | } 494 | 495 | --mcus; 496 | 497 | if (!mcus) { 498 | --intervals; 499 | if (!intervals) 500 | return; 501 | 502 | read_restart(dc.stream); // RSTx marker 503 | 504 | if (intervals == 1) { 505 | // last interval, may have fewer MCUs than defined by DRI 506 | mcus = (dc.num_mcu_y - mcu_j - 1) 507 | * dc.num_mcu_x + dc.num_mcu_x - mcu_i - 1; 508 | } else { 509 | mcus = dc.restart_interval; 510 | } 511 | 512 | // reset decoder 513 | dc.cb = 0; 514 | dc.bits_left = 0; 515 | foreach (k; 0..dc.num_comps) 516 | dc.comps[k].pred = 0; 517 | } 518 | 519 | } 520 | } 521 | } 522 | 523 | // RST0-RST7 524 | void read_restart(Reader stream) { 525 | ubyte[2] tmp = void; 526 | stream.readExact(tmp, tmp.length); 527 | if (tmp[0] != 0xff || tmp[1] < Marker.RST0 || Marker.RST7 < tmp[1]) 528 | throw new ImageIOException("reset marker missing"); 529 | // the markers should cycle 0 through 7, could check that here... 530 | } 531 | 532 | immutable ubyte[64] dezigzag = [ 533 | 0, 1, 8, 16, 9, 2, 3, 10, 534 | 17, 24, 32, 25, 18, 11, 4, 5, 535 | 12, 19, 26, 33, 40, 48, 41, 34, 536 | 27, 20, 13, 6, 7, 14, 21, 28, 537 | 35, 42, 49, 56, 57, 50, 43, 36, 538 | 29, 22, 15, 23, 30, 37, 44, 51, 539 | 58, 59, 52, 45, 38, 31, 39, 46, 540 | 53, 60, 61, 54, 47, 55, 62, 63, 541 | ]; 542 | 543 | // decode entropy, dequantize & dezigzag (see section F.2) 544 | short[64] decode_block(ref JPEG_Decoder dc, ref JPEG_Decoder.Component comp, 545 | in ref ubyte[64] qtable) 546 | { 547 | short[64] res = 0; 548 | 549 | ubyte t = decode_huff(dc, dc.dc_tables[comp.dc_table]); 550 | int diff = t ? dc.receive_and_extend(t) : 0; 551 | 552 | comp.pred = comp.pred + diff; 553 | res[0] = cast(short) (comp.pred * qtable[0]); 554 | 555 | int k = 1; 556 | do { 557 | ubyte rs = decode_huff(dc, dc.ac_tables[comp.ac_table]); 558 | ubyte rrrr = rs >> 4; 559 | ubyte ssss = rs & 0xf; 560 | 561 | if (ssss == 0) { 562 | if (rrrr != 0xf) 563 | break; // end of block 564 | k += 16; // run length is 16 565 | continue; 566 | } 567 | 568 | k += rrrr; 569 | 570 | if (63 < k) 571 | throw new ImageIOException("corrupt block"); 572 | res[dezigzag[k]] = cast(short) (dc.receive_and_extend(ssss) * qtable[k]); 573 | k += 1; 574 | } while (k < 64); 575 | 576 | return res; 577 | } 578 | 579 | int receive_and_extend(ref JPEG_Decoder dc, ubyte s) { 580 | // receive 581 | int symbol = 0; 582 | foreach (_; 0..s) 583 | symbol = (symbol << 1) + nextbit(dc); 584 | // extend 585 | int vt = 1 << (s-1); 586 | if (symbol < vt) 587 | return symbol + (-1 << s) + 1; 588 | return symbol; 589 | } 590 | 591 | // F.16 -- the DECODE 592 | ubyte decode_huff(ref JPEG_Decoder dc, in ref HuffTab tab) { 593 | short code = nextbit(dc); 594 | 595 | int i = 0; 596 | while (tab.maxcode[i] < code) { 597 | code = cast(short) ((code << 1) + nextbit(dc)); 598 | i += 1; 599 | if (tab.maxcode.length <= i) 600 | throw new ImageIOException("corrupt huffman coding"); 601 | } 602 | int j = tab.valptr[i] + code - tab.mincode[i]; 603 | if (tab.values.length <= cast(uint) j) 604 | throw new ImageIOException("corrupt huffman coding"); 605 | return tab.values[j]; 606 | } 607 | 608 | // F.2.2.5 and F.18 609 | ubyte nextbit(ref JPEG_Decoder dc) { 610 | if (!dc.bits_left) { 611 | ubyte[1] bytebuf; 612 | dc.stream.readExact(bytebuf, 1); 613 | dc.cb = bytebuf[0]; 614 | dc.bits_left = 8; 615 | 616 | if (dc.cb == 0xff) { 617 | dc.stream.readExact(bytebuf, 1); 618 | if (bytebuf[0] != 0x0) 619 | throw new ImageIOException("unexpected marker"); 620 | } 621 | } 622 | 623 | ubyte r = dc.cb >> 7; 624 | dc.cb <<= 1; 625 | dc.bits_left -= 1; 626 | return r; 627 | } 628 | 629 | ubyte[] reconstruct(in ref JPEG_Decoder dc) { 630 | auto result = new ubyte[dc.width * dc.height * dc.tgt_chans]; 631 | 632 | switch (dc.num_comps * 10 + dc.tgt_chans) { 633 | case 34, 33: 634 | // Use specialized bilinear filtering functions for the frequent cases where 635 | // Cb & Cr channels have half resolution. 636 | if ((dc.comps[0].sfx <= 2 && dc.comps[0].sfy <= 2) 637 | && (dc.comps[0].sfx + dc.comps[0].sfy >= 3) 638 | && dc.comps[1].sfx == 1 && dc.comps[1].sfy == 1 639 | && dc.comps[2].sfx == 1 && dc.comps[2].sfy == 1) { 640 | void function(in ubyte[], in ubyte[], ubyte[]) resample; 641 | switch (dc.comps[0].sfx * 10 + dc.comps[0].sfy) { 642 | case 22: resample = &upsample_h2_v2; break; 643 | case 21: resample = &upsample_h2_v1; break; 644 | case 12: resample = &upsample_h1_v2; break; 645 | default: throw new ImageIOException("bug"); 646 | } 647 | 648 | auto comp1 = new ubyte[dc.width]; 649 | auto comp2 = new ubyte[dc.width]; 650 | 651 | size_t s = 0; 652 | size_t di = 0; 653 | foreach (j; 0 .. dc.height) { 654 | size_t mi = j / dc.comps[0].sfy; 655 | size_t si = (mi == 0 || mi >= (dc.height-1)/dc.comps[0].sfy) 656 | ? mi : mi - 1 + s * 2; 657 | s = s ^ 1; 658 | 659 | size_t cs = dc.num_mcu_x * dc.comps[1].sfx * 8; 660 | size_t cl0 = mi * cs; 661 | size_t cl1 = si * cs; 662 | resample(dc.comps[1].data[cl0 .. cl0 + dc.comps[1].x], 663 | dc.comps[1].data[cl1 .. cl1 + dc.comps[1].x], 664 | comp1[]); 665 | resample(dc.comps[2].data[cl0 .. cl0 + dc.comps[2].x], 666 | dc.comps[2].data[cl1 .. cl1 + dc.comps[2].x], 667 | comp2[]); 668 | 669 | foreach (i; 0 .. dc.width) { 670 | result[di .. di+3] = ycbcr_to_rgb( 671 | dc.comps[0].data[j * dc.num_mcu_x * dc.comps[0].sfx * 8 + i], 672 | comp1[i], 673 | comp2[i], 674 | ); 675 | if (dc.tgt_chans == 4) 676 | result[di+3] = 255; 677 | di += dc.tgt_chans; 678 | } 679 | } 680 | 681 | return result; 682 | } 683 | 684 | foreach (const ref comp; dc.comps[0..dc.num_comps]) { 685 | if (comp.sfx != dc.hmax || comp.sfy != dc.vmax) 686 | return dc.upsample(result); 687 | } 688 | 689 | size_t si, di; 690 | foreach (j; 0 .. dc.height) { 691 | foreach (i; 0 .. dc.width) { 692 | result[di .. di+3] = ycbcr_to_rgb( 693 | dc.comps[0].data[si+i], 694 | dc.comps[1].data[si+i], 695 | dc.comps[2].data[si+i], 696 | ); 697 | if (dc.tgt_chans == 4) 698 | result[di+3] = 255; 699 | di += dc.tgt_chans; 700 | } 701 | si += dc.num_mcu_x * dc.comps[0].sfx * 8; 702 | } 703 | return result; 704 | case 32, 12, 31, 11: 705 | const comp = &dc.comps[0]; 706 | if (comp.sfx == dc.hmax && comp.sfy == dc.vmax) { 707 | size_t si, di; 708 | if (dc.tgt_chans == 2) { 709 | foreach (j; 0 .. dc.height) { 710 | foreach (i; 0 .. dc.width) { 711 | result[di++] = comp.data[si+i]; 712 | result[di++] = 255; 713 | } 714 | si += dc.num_mcu_x * comp.sfx * 8; 715 | } 716 | } else { 717 | foreach (j; 0 .. dc.height) { 718 | result[di .. di+dc.width] = comp.data[si .. si+dc.width]; 719 | si += dc.num_mcu_x * comp.sfx * 8; 720 | di += dc.width; 721 | } 722 | } 723 | return result; 724 | } else { 725 | // need to resample (haven't tested this...) 726 | return dc.upsample_luma(result); 727 | } 728 | case 14, 13: 729 | const comp = &dc.comps[0]; 730 | size_t si, di; 731 | foreach (j; 0 .. dc.height) { 732 | foreach (i; 0 .. dc.width) { 733 | result[di .. di+3] = comp.data[si+i]; 734 | if (dc.tgt_chans == 4) 735 | result[di+3] = 255; 736 | di += dc.tgt_chans; 737 | } 738 | si += dc.num_mcu_x * comp.sfx * 8; 739 | } 740 | return result; 741 | default: assert(0); 742 | } 743 | } 744 | 745 | void upsample_h2_v2(in ubyte[] line0, in ubyte[] line1, ubyte[] result) { 746 | ubyte mix(ubyte mm, ubyte ms, ubyte sm, ubyte ss) { 747 | return cast(ubyte) (( cast(uint) mm * 3 * 3 748 | + cast(uint) ms * 3 * 1 749 | + cast(uint) sm * 1 * 3 750 | + cast(uint) ss * 1 * 1 751 | + 8) / 16); 752 | } 753 | 754 | result[0] = cast(ubyte) (( cast(uint) line0[0] * 3 755 | + cast(uint) line1[0] * 1 756 | + 2) / 4); 757 | if (line0.length == 1) return; 758 | result[1] = mix(line0[0], line0[1], line1[0], line1[1]); 759 | 760 | size_t di = 2; 761 | foreach (i; 1 .. line0.length) { 762 | result[di] = mix(line0[i], line0[i-1], line1[i], line1[i-1]); 763 | di += 1; 764 | if (i == line0.length-1) { 765 | if (di < result.length) { 766 | result[di] = cast(ubyte) (( cast(uint) line0[i] * 3 767 | + cast(uint) line1[i] * 1 768 | + 2) / 4); 769 | } 770 | return; 771 | } 772 | result[di] = mix(line0[i], line0[i+1], line1[i], line1[i+1]); 773 | di += 1; 774 | } 775 | } 776 | 777 | void upsample_h2_v1(in ubyte[] line0, in ubyte[] _line1, ubyte[] result) { 778 | result[0] = line0[0]; 779 | if (line0.length == 1) return; 780 | result[1] = cast(ubyte) (( cast(uint) line0[0] * 3 781 | + cast(uint) line0[1] * 1 782 | + 2) / 4); 783 | size_t di = 2; 784 | foreach (i; 1 .. line0.length) { 785 | result[di] = cast(ubyte) (( cast(uint) line0[i-1] * 1 786 | + cast(uint) line0[i+0] * 3 787 | + 2) / 4); 788 | di += 1; 789 | if (i == line0.length-1) { 790 | if (di < result.length) result[di] = line0[i]; 791 | return; 792 | } 793 | result[di] = cast(ubyte) (( cast(uint) line0[i+0] * 3 794 | + cast(uint) line0[i+1] * 1 795 | + 2) / 4); 796 | di += 1; 797 | } 798 | } 799 | 800 | void upsample_h1_v2(in ubyte[] line0, in ubyte[] line1, ubyte[] result) { 801 | foreach (i; 0 .. result.length) { 802 | result[i] = cast(ubyte) (( cast(uint) line0[i] * 3 803 | + cast(uint) line1[i] * 1 804 | + 2) / 4); 805 | } 806 | } 807 | 808 | // Nearest neighbor 809 | ubyte[] upsample_luma(in ref JPEG_Decoder dc, ubyte[] result) { 810 | const size_t stride0 = dc.num_mcu_x * dc.comps[0].sfx * 8; 811 | const y_step0 = cast(float) dc.comps[0].sfy / cast(float) dc.vmax; 812 | const x_step0 = cast(float) dc.comps[0].sfx / cast(float) dc.hmax; 813 | 814 | float y0 = y_step0 * 0.5f; 815 | size_t y0i = 0; 816 | 817 | size_t di; 818 | 819 | foreach (j; 0 .. dc.height) { 820 | float x0 = x_step0 * 0.5f; 821 | size_t x0i = 0; 822 | foreach (i; 0 .. dc.width) { 823 | result[di] = dc.comps[0].data[y0i + x0i]; 824 | if (dc.tgt_chans == 2) 825 | result[di+1] = 255; 826 | di += dc.tgt_chans; 827 | x0 += x_step0; 828 | if (x0 >= 1.0f) { x0 -= 1.0f; x0i += 1; } 829 | } 830 | y0 += y_step0; 831 | if (y0 >= 1.0f) { y0 -= 1.0f; y0i += stride0; } 832 | } 833 | return result; 834 | } 835 | 836 | // Nearest neighbor 837 | ubyte[] upsample(in ref JPEG_Decoder dc, ubyte[] result) { 838 | const size_t stride0 = dc.num_mcu_x * dc.comps[0].sfx * 8; 839 | const size_t stride1 = dc.num_mcu_x * dc.comps[1].sfx * 8; 840 | const size_t stride2 = dc.num_mcu_x * dc.comps[2].sfx * 8; 841 | 842 | const y_step0 = cast(float) dc.comps[0].sfy / cast(float) dc.vmax; 843 | const y_step1 = cast(float) dc.comps[1].sfy / cast(float) dc.vmax; 844 | const y_step2 = cast(float) dc.comps[2].sfy / cast(float) dc.vmax; 845 | const x_step0 = cast(float) dc.comps[0].sfx / cast(float) dc.hmax; 846 | const x_step1 = cast(float) dc.comps[1].sfx / cast(float) dc.hmax; 847 | const x_step2 = cast(float) dc.comps[2].sfx / cast(float) dc.hmax; 848 | 849 | float y0 = y_step0 * 0.5f; 850 | float y1 = y_step1 * 0.5f; 851 | float y2 = y_step2 * 0.5f; 852 | size_t y0i = 0; 853 | size_t y1i = 0; 854 | size_t y2i = 0; 855 | 856 | size_t di; 857 | 858 | foreach (_j; 0 .. dc.height) { 859 | float x0 = x_step0 * 0.5f; 860 | float x1 = x_step1 * 0.5f; 861 | float x2 = x_step2 * 0.5f; 862 | size_t x0i = 0; 863 | size_t x1i = 0; 864 | size_t x2i = 0; 865 | foreach (i; 0 .. dc.width) { 866 | result[di .. di+3] = ycbcr_to_rgb( 867 | dc.comps[0].data[y0i + x0i], 868 | dc.comps[1].data[y1i + x1i], 869 | dc.comps[2].data[y2i + x2i], 870 | ); 871 | if (dc.tgt_chans == 4) 872 | result[di+3] = 255; 873 | di += dc.tgt_chans; 874 | x0 += x_step0; 875 | x1 += x_step1; 876 | x2 += x_step2; 877 | if (x0 >= 1.0) { x0 -= 1.0f; x0i += 1; } 878 | if (x1 >= 1.0) { x1 -= 1.0f; x1i += 1; } 879 | if (x2 >= 1.0) { x2 -= 1.0f; x2i += 1; } 880 | } 881 | y0 += y_step0; 882 | y1 += y_step1; 883 | y2 += y_step2; 884 | if (y0 >= 1.0) { y0 -= 1.0f; y0i += stride0; } 885 | if (y1 >= 1.0) { y1 -= 1.0f; y1i += stride1; } 886 | if (y2 >= 1.0) { y2 -= 1.0f; y2i += stride2; } 887 | } 888 | return result; 889 | } 890 | 891 | ubyte[3] ycbcr_to_rgb(ubyte y, ubyte cb, ubyte cr) pure { 892 | ubyte[3] rgb = void; 893 | rgb[0] = clamp(y + 1.402*(cr-128)); 894 | rgb[1] = clamp(y - 0.34414*(cb-128) - 0.71414*(cr-128)); 895 | rgb[2] = clamp(y + 1.772*(cb-128)); 896 | return rgb; 897 | } 898 | 899 | ubyte clamp(float x) pure { 900 | if (x < 0) return 0; 901 | if (255 < x) return 255; 902 | return cast(ubyte) x; 903 | } 904 | 905 | // ------------------------------------------------------------ 906 | // The IDCT stuff here (to the next dashed line) is copied and adapted from 907 | // stb_image which is released under public domain. Many thanks to stb_image 908 | // author, Sean Barrett. 909 | // Link: https://github.com/nothings/stb/blob/master/stb_image.h 910 | 911 | pure int f2f(float x) { return cast(int) (x * 4096 + 0.5); } 912 | pure int fsh(int x) { return x << 12; } 913 | 914 | // from stb_image, derived from jidctint -- DCT_ISLOW 915 | pure void STBI__IDCT_1D(ref int t0, ref int t1, ref int t2, ref int t3, 916 | ref int x0, ref int x1, ref int x2, ref int x3, 917 | int s0, int s1, int s2, int s3, int s4, int s5, int s6, int s7) 918 | { 919 | int p1,p2,p3,p4,p5; 920 | //int t0,t1,t2,t3,p1,p2,p3,p4,p5,x0,x1,x2,x3; 921 | p2 = s2; 922 | p3 = s6; 923 | p1 = (p2+p3) * f2f(0.5411961f); 924 | t2 = p1 + p3 * f2f(-1.847759065f); 925 | t3 = p1 + p2 * f2f( 0.765366865f); 926 | p2 = s0; 927 | p3 = s4; 928 | t0 = fsh(p2+p3); 929 | t1 = fsh(p2-p3); 930 | x0 = t0+t3; 931 | x3 = t0-t3; 932 | x1 = t1+t2; 933 | x2 = t1-t2; 934 | t0 = s7; 935 | t1 = s5; 936 | t2 = s3; 937 | t3 = s1; 938 | p3 = t0+t2; 939 | p4 = t1+t3; 940 | p1 = t0+t3; 941 | p2 = t1+t2; 942 | p5 = (p3+p4)*f2f( 1.175875602f); 943 | t0 = t0*f2f( 0.298631336f); 944 | t1 = t1*f2f( 2.053119869f); 945 | t2 = t2*f2f( 3.072711026f); 946 | t3 = t3*f2f( 1.501321110f); 947 | p1 = p5 + p1*f2f(-0.899976223f); 948 | p2 = p5 + p2*f2f(-2.562915447f); 949 | p3 = p3*f2f(-1.961570560f); 950 | p4 = p4*f2f(-0.390180644f); 951 | t3 += p1+p4; 952 | t2 += p2+p3; 953 | t1 += p2+p4; 954 | t0 += p1+p3; 955 | } 956 | 957 | // idct and level-shift 958 | pure void stbi__idct_block(ubyte* dst, int dst_stride, in ref short[64] data) { 959 | int i; 960 | int[64] val; 961 | int* v = val.ptr; 962 | const(short)* d = data.ptr; 963 | 964 | // columns 965 | for (i=0; i < 8; ++i,++d, ++v) { 966 | // if all zeroes, shortcut -- this avoids dequantizing 0s and IDCTing 967 | if (d[ 8]==0 && d[16]==0 && d[24]==0 && d[32]==0 968 | && d[40]==0 && d[48]==0 && d[56]==0) { 969 | // no shortcut 0 seconds 970 | // (1|2|3|4|5|6|7)==0 0 seconds 971 | // all separate -0.047 seconds 972 | // 1 && 2|3 && 4|5 && 6|7: -0.047 seconds 973 | int dcterm = d[0] << 2; 974 | v[0] = v[8] = v[16] = v[24] = v[32] = v[40] = v[48] = v[56] = dcterm; 975 | } else { 976 | int t0,t1,t2,t3,x0,x1,x2,x3; 977 | STBI__IDCT_1D( 978 | t0, t1, t2, t3, 979 | x0, x1, x2, x3, 980 | d[ 0], d[ 8], d[16], d[24], 981 | d[32], d[40], d[48], d[56] 982 | ); 983 | // constants scaled things up by 1<<12; let's bring them back 984 | // down, but keep 2 extra bits of precision 985 | x0 += 512; x1 += 512; x2 += 512; x3 += 512; 986 | v[ 0] = (x0+t3) >> 10; 987 | v[56] = (x0-t3) >> 10; 988 | v[ 8] = (x1+t2) >> 10; 989 | v[48] = (x1-t2) >> 10; 990 | v[16] = (x2+t1) >> 10; 991 | v[40] = (x2-t1) >> 10; 992 | v[24] = (x3+t0) >> 10; 993 | v[32] = (x3-t0) >> 10; 994 | } 995 | } 996 | 997 | ubyte* o = dst; 998 | for (i=0, v=val.ptr; i < 8; ++i,v+=8,o+=dst_stride) { 999 | // no fast case since the first 1D IDCT spread components out 1000 | int t0,t1,t2,t3,x0,x1,x2,x3; 1001 | STBI__IDCT_1D( 1002 | t0, t1, t2, t3, 1003 | x0, x1, x2, x3, 1004 | v[0],v[1],v[2],v[3],v[4],v[5],v[6],v[7] 1005 | ); 1006 | // constants scaled things up by 1<<12, plus we had 1<<2 from first 1007 | // loop, plus horizontal and vertical each scale by sqrt(8) so together 1008 | // we've got an extra 1<<3, so 1<<17 total we need to remove. 1009 | // so we want to round that, which means adding 0.5 * 1<<17, 1010 | // aka 65536. Also, we'll end up with -128 to 127 that we want 1011 | // to encode as 0-255 by adding 128, so we'll add that before the shift 1012 | x0 += 65536 + (128<<17); 1013 | x1 += 65536 + (128<<17); 1014 | x2 += 65536 + (128<<17); 1015 | x3 += 65536 + (128<<17); 1016 | // tried computing the shifts into temps, or'ing the temps to see 1017 | // if any were out of range, but that was slower 1018 | o[0] = stbi__clamp((x0+t3) >> 17); 1019 | o[7] = stbi__clamp((x0-t3) >> 17); 1020 | o[1] = stbi__clamp((x1+t2) >> 17); 1021 | o[6] = stbi__clamp((x1-t2) >> 17); 1022 | o[2] = stbi__clamp((x2+t1) >> 17); 1023 | o[5] = stbi__clamp((x2-t1) >> 17); 1024 | o[3] = stbi__clamp((x3+t0) >> 17); 1025 | o[4] = stbi__clamp((x3-t0) >> 17); 1026 | } 1027 | } 1028 | 1029 | // clamp to 0-255 1030 | pure ubyte stbi__clamp(int x) { 1031 | if (cast(uint) x > 255) { 1032 | if (x < 0) return 0; 1033 | if (x > 255) return 255; 1034 | } 1035 | return cast(ubyte) x; 1036 | } 1037 | 1038 | // ------------------------------------------------------------ 1039 | --------------------------------------------------------------------------------