d2dimage: better png writer
[dd2d.git] / d2dimage.d
blob8917ea72250b560939a1b4c6147563a4c24b1c14
1 /* DooM2D: Midnight on the Firing Line
2 * coded by Ketmar // Invisible Vector <ketmar@ketmar.no-ip.org>
3 * Understanding is not required. Only obedience.
5 * This program is free software: you can redistribute it and/or modify
6 * it under the terms of the GNU General Public License as published by
7 * the Free Software Foundation, either version 3 of the License, or
8 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
15 * You should have received a copy of the GNU General Public License
16 * along with this program. If not, see <http://www.gnu.org/licenses/>.
18 module d2dimage is aliced;
19 private:
21 import arsd.color;
22 import arsd.png;
23 import iv.vfs;
24 import iv.glbinds;
26 import glutils;
27 import console;
28 import wadarc;
29 import jpeg;
32 // ////////////////////////////////////////////////////////////////////////// //
33 public __gshared Color[256] d2dpal;
36 public void loadD2DPalette () {
37 ubyte[768] vgapal;
39 auto fl = openFile("playpal.pal");
40 fl.rawReadExact(vgapal[]);
41 foreach (ref ubyte b; vgapal) if (b > 63) b = 63; // just in case
43 foreach (immutable idx; 0..256) {
44 d2dpal[idx].r = cast(ubyte)(vgapal[idx*3+0]*255/63);
45 d2dpal[idx].g = cast(ubyte)(vgapal[idx*3+1]*255/63);
46 d2dpal[idx].b = cast(ubyte)(vgapal[idx*3+2]*255/63);
47 d2dpal[idx].a = 255;
49 // color 0 is transparent
50 d2dpal[0].asUint = 0;
54 // ////////////////////////////////////////////////////////////////////////// //
55 public final class D2DImage {
56 public:
57 int sx, sy;
58 int mwidth, mheight;
59 private TrueColorImage mimg;
60 private Texture mtex;
62 private this () pure nothrow @safe {}
64 this (string name) {
65 import std.path : extension;
66 auto fl = openFile(name);
67 conwriteln("loading image '", name, "'");
68 load(fl);
71 this (int awdt, int ahgt) nothrow @trusted {
72 assert(awdt >= 0 && ahgt >= 0);
73 sx = sy = 0;
74 mwidth = awdt;
75 mheight = ahgt;
76 if (awdt > 0 && ahgt > 0) {
77 try {
78 mimg = new TrueColorImage(awdt, ahgt);
79 } catch (Exception e) {
80 assert(0, e.toString);
82 mimg.imageData.bytes[] = 0;
86 /// will not clone texture!
87 D2DImage clone () const nothrow @trusted {
88 auto res = new D2DImage();
89 res.sx = sx;
90 res.sy = sy;
91 res.mwidth = mwidth;
92 res.mheight = mheight;
93 if (valid) {
94 try {
95 res.mimg = new TrueColorImage(mwidth, mheight);
96 } catch (Exception e) {
97 assert(0, e.toString);
99 res.mimg.imageData.bytes[] = mimg.imageData.bytes[];
101 return res;
104 void resize (int awdt, int ahgt) nothrow @trusted {
105 assert(awdt >= 0 && ahgt >= 0);
106 if (mwidth != awdt || mheight != ahgt) {
107 mtex = null;
108 mwidth = awdt;
109 mheight = ahgt;
110 sx = sy = 0;
111 if (awdt > 0 && ahgt > 0) {
112 try {
113 mimg = new TrueColorImage(awdt, ahgt);
114 } catch (Exception e) {
115 assert(0, e.toString);
117 mimg.imageData.bytes[] = 0;
120 if (mwidth <= 0 || mheight <= 0) mimg = null;
123 void clear () {
124 //if (mtex !is null) mtex.clear;
125 //if (mimg !is null) mimg.clear;
126 mtex = null;
127 mimg = null;
128 mwidth = mheight = 0;
129 sx = sy = 0;
132 void removeOffset () { sx = sy = 0; }
134 @property const pure nothrow @safe @nogc {
135 bool valid () { pragma(inline, true); return (mimg !is null && mwidth > 0 && mheight > 0); }
136 int width () { pragma(inline, true); return mwidth; }
137 int height () { pragma(inline, true); return mheight; }
139 // DO NOT RESIZE!
140 @property TrueColorImage img () pure nothrow @safe @nogc { pragma(inline, true); return mimg; }
142 Color opIndex (usize y, usize x) const /*pure*/ nothrow @safe @nogc { pragma(inline, true); return getPixel(x, y); }
143 void opIndex (Color clr, usize y, usize x) nothrow @safe @nogc { pragma(inline, true); setPixel(x, y, clr); }
144 void opIndex (Color clr, usize y, usize x, bool ignoreTransparent) nothrow @safe @nogc { pragma(inline, true); if (!ignoreTransparent || clr.a != 0) setPixel(x, y, clr); }
146 Color getPixel (int x, int y) const /*pure*/ nothrow @trusted @nogc {
147 pragma(inline, true);
148 return (mimg !is null && x >= 0 && y >= 0 && x < mwidth && y < mheight ? (cast(const(Color*))mimg.imageData.bytes.ptr)[y*mwidth+x] : Color(0, 0, 0, 0));
151 void setPixel (int x, int y, Color clr) nothrow @trusted @nogc {
152 pragma(inline, true);
153 if (mimg !is null && x >= 0 && y >= 0 && x < mwidth && y < mheight) (cast(Color*)mimg.imageData.bytes.ptr)[y*mwidth+x] = clr;
156 void putPixel (int x, int y, Color clr) nothrow @trusted @nogc {
157 pragma(inline, true);
158 if (mimg !is null && x >= 0 && y >= 0 && x < mwidth && y < mheight && clr.a != 0) (cast(Color*)mimg.imageData.bytes.ptr)[y*mwidth+x] = clr;
161 void toBlackAndWhite () {
162 if (!valid) return;
163 foreach (int y; 0..mheight) {
164 foreach (int x; 0..mwidth) {
165 Color clr = getPixel(x, y);
166 int i = cast(int)(0.2126*clr.r+0.7152*clr.g+0.0722*clr.b);
167 if (i > 255) i = 255; // just in case
168 setPixel(x, y, Color(i&0xff, i&0xff, i&0xff, clr.a));
173 void toBlackAndWhiteChan(string chan) () {
174 static assert(chan == "red" || chan == "r" || chan == "green" || chan == "g" || chan == "blue" || chan == "b", "invalid channel");
175 if (!valid) return;
176 foreach (int y; 0..mheight) {
177 foreach (int x; 0..mwidth) {
178 Color clr = getPixel(x, y);
179 static if (chan == "red" || chan == "r") ubyte i = clr.r;
180 else static if (chan == "green" || chan == "g") ubyte i = clr.g;
181 else static if (chan == "blue" || chan == "b") ubyte i = clr.b;
182 else static assert(0, "wtf?!");
183 setPixel(x, y, Color(i, i, i, clr.a));
188 D2DImage blackAndWhite () {
189 auto res = this.clone();
190 res.toBlackAndWhite;
191 return res;
194 D2DImage blackAndWhiteChan(string chan) () {
195 auto res = this.clone();
196 res.toBlackAndWhiteChan!chan;
197 return res;
200 /// mirror image horizontally
201 void mirror () {
202 if (!valid) return;
203 foreach (int y; 0..height) {
204 foreach (int x; 0..width/2) {
205 int mx = width-x-1;
206 auto c0 = getPixel(x, y);
207 auto c1 = getPixel(mx, y);
208 setPixel(x, y, c1);
209 setPixel(mx, y, c0);
212 sx = width-sx-1;
215 void createTex () {
216 if (mtex is null && valid) mtex = new Texture(mimg, Texture.Option.Nearest);
219 void updateTex () {
220 if (valid) {
221 if (mtex is null) mtex = new Texture(mimg, Texture.Option.Nearest); else mtex.setFromImage(mimg);
225 GLuint asTex () {
226 if (!valid) return 0;
227 if (mtex is null) createTex();
228 return (mtex !is null ? mtex.id : 0);
231 void savePng (string fname) {
232 if (!valid) throw new Exception("can't save empty image");
233 auto png = pngFromImage(mimg);
235 void insertChunk (Chunk *chk) {
236 foreach (immutable idx; 0..png.chunks.length) {
237 if (cast(string)png.chunks[idx].type == "IDAT") {
238 // ok, insert before IDAT
239 png.chunks.length += 1;
240 foreach_reverse (immutable c; idx+1..png.chunks.length) png.chunks[c] = png.chunks[c-1];
241 png.chunks[idx] = *chk;
242 return;
245 png.chunks ~= *chk;
248 if (sx != 0 || sy != 0) {
250 D2DInfoChunk di;
251 di.ver = 0;
252 di.sx = sx;
253 di.sy = sy;
254 di.fixEndian;
255 auto chk = Chunk.create("dtDi", (cast(ubyte*)&di)[0..di.sizeof]);
256 //png.chunks ~= *chk;
257 insertChunk(chk);
259 // zdoom chunk
260 if (sx >= short.min && sx <= short.max && sy >= short.min && sy <= short.max) {
261 GrabChunk di;
262 di.sx = cast(short)sx;
263 di.sy = cast(short)sy;
264 di.fixEndian;
265 auto chk = Chunk.create("grAb", (cast(ubyte*)&di)[0..di.sizeof]);
266 //png.chunks ~= *chk;
267 insertChunk(chk);
270 auto fo = vfsDiskOpen(fname, "w");
271 fo.rawWriteExact(writePng(png));
274 private:
275 static align(1) struct D2DInfoChunk {
276 align(1):
277 ubyte ver; // version; 0 for now; versions should be compatible
278 int sx, sy;
280 void fixEndian () nothrow @trusted @nogc {
281 version(BigEndian) {
282 import std.bitmanip : swapEndian;
283 sx = swapEndian(sx);
284 sy = swapEndian(sy);
289 static align(1) struct GrabChunk {
290 align(1):
291 short sx, sy;
293 void fixEndian () nothrow @trusted @nogc {
294 version(LittleEndian) {
295 import std.bitmanip : swapEndian;
296 sx = swapEndian(sx);
297 sy = swapEndian(sy);
302 void loadPng (VFile fl) {
303 auto flsize = fl.size-fl.tell;
304 if (flsize < 8 || flsize > 1024*1024*32) throw new Exception("png image too big");
305 auto data = new ubyte[](cast(uint)flsize);
306 fl.rawReadExact(data);
307 auto png = readPng(data);
308 auto ximg = imageFromPng(png).getAsTrueColorImage;
309 if (ximg is null) throw new Exception("png: wtf?!");
310 if (ximg.width < 1 || ximg.height < 1) throw new Exception("png image too small");
311 mwidth = ximg.width;
312 mheight = ximg.height;
313 sx = sy = 0;
314 mimg = ximg;
315 foreach (ref chk; png.chunks) {
316 if (chk.type[] == "dtDi") {
317 // d2d info chunk
318 if (chk.size >= D2DInfoChunk.sizeof) {
319 auto di = *cast(D2DInfoChunk*)chk.payload.ptr;
320 di.fixEndian;
321 sx = di.sx;
322 sy = di.sy;
323 version(png_dump_chunks) conwriteln("found 'dtDi' chunk! sx=", di.sx, "; sy=", di.sy);
325 } else if (chk.type[] == "grAb") {
326 // zdoom info chunk
327 if (chk.size >= GrabChunk.sizeof) {
328 auto di = *cast(GrabChunk*)chk.payload.ptr;
329 di.fixEndian;
330 sx = di.sx;
331 sy = di.sy;
332 version(png_dump_chunks) conwriteln("found 'grAb' chunk! sx=", di.sx, "; sy=", di.sy);
338 void loadVga (VFile fl) {
339 //conwriteln(" loading .VGA image");
340 auto w = fl.readNum!ushort();
341 auto h = fl.readNum!ushort();
342 auto isx = fl.readNum!short();
343 auto isy = fl.readNum!short();
344 //conwriteln(" loading .VGA image; w=", w, "; h=", h, "; isx=", isx, "; isy=", isy);
345 if (w < 1 || w > 32760) throw new Exception("invalid vga image width");
346 if (h < 1 || h > 32760) throw new Exception("invalid vga image height");
347 auto data = new ubyte[](w*h);
348 fl.rawReadExact(data[]);
349 resize(w, h);
350 assert(mimg !is null);
351 assert(mimg.width == w);
352 assert(mimg.height == h);
353 assert(mwidth == w);
354 assert(mheight == h);
355 //conwriteln(" !!!");
356 sx = isx;
357 sy = isy;
358 foreach (int y; 0..height) {
359 foreach (int x; 0..width) {
360 setPixel(x, y, d2dpal.ptr[data.ptr[y*w+x]]);
365 void loadJpeg (VFile fl) {
366 auto jpg = new JpegDecoder(fl, JpegDecoder.Upsampling.BILINEAR);
367 if (jpg.image.width < 1 || jpg.image.width > 32760) throw new Exception("invalid image width");
368 if (jpg.image.height < 1 || jpg.image.height > 32760) throw new Exception("invalid image height");
369 mtex = null;
370 mwidth = jpg.image.width;
371 mheight = jpg.image.height;
372 sx = sy = 0;
373 mimg = jpg.image;
376 void load (VFile fl) {
377 scope(failure) fl.seek(0);
378 char[8] sign;
379 fl.seek(0);
380 fl.rawReadExact(sign[]);
381 fl.seek(0);
382 // png?
383 if (sign == "\x89\x50\x4E\x47\x0D\x0A\x1A\x0A") {
384 loadPng(fl);
385 return;
387 // jpeg?
388 if (sign[0..2] == "\xff\xd8") {
389 fl.seek(-2, Seek.End);
390 fl.rawReadExact(sign[0..2]);
391 fl.seek(0);
392 if (sign[0..2] == "\xff\xd9") {
393 loadJpeg(fl);
394 return;
397 // alas, this must be vga
398 loadVga(fl);