root/trunk/lodepng/lodepng/Encode.d

Revision 217, 26.3 kB (checked in by Lutger, 10 months ago)

bugfixes and DMD compatibility

Line 
1 /***************************************************************************************************
2 License:
3 Copyright (c) 2005-2007 Lode Vandevenne
4 All rights reserved.
5
6 Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
7
8   - Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.<br>
9   - Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution.<br>
10   - Neither the name of Lode Vandevenne nor the names of his contributors may be used to endorse or promote products derived from this software without specific prior written permission.<br>
11
12 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
13
14 Authors: Lode Vandevenne (original version in C++), Lutger Blijdestijn (D version) : lutger dot blijdestijn at gmail dot com,
15   Stewart Gordon (bug fixes)
16
17 Date: Januari 16, 2008
18
19 About:
20 The lodepng encoder can encode images of any of the supported png color formats to 24-bit RGB or
21 32-bit RGBA png images. Conversion, if needed, is done automatically. It is compatible with the
22 Phobos and Tango libraries. <br>
23 This module publicly imports lodepng.Common, where you'll find the data types used by both the encoder
24 and decoder, as well as some utility and image format conversion routines.<br>
25
26 Features:
27 The following features are understood by the encoder:
28 <ul>
29     <li> conformant encoding of 24-bit RGB and 32-bit RGBA PNG images </li>
30     <li> automatic conversion of other color formats </li>
31     <li> setting the compression and filter methods </li>
32     <li> textual key-value metadata: normal and compressed latin1, unicode (utf-8) </li>
33     <li> transparency / colorkey </li>
34     <li> encoding raw chunks </li>
35     <li> the following chunks are written by the encoder
36         <ul>
37             <li>IHDR (image information)</li>
38             <li>IDAT (pixel data)</li>
39             <li>IEND (the final chunk)</li>
40             <li>tRNS (colorkey)</li>
41             <li>bKGD (suggested background color) </li>
42             <li>tEXt (uncompressed latin-1 key-value strings)</li>
43             <li>zTXt (compressed latin-1 key-value strings)</li>
44             <li>iTXt (utf8 key-value strings)</li>
45         </ul>
46    </li>
47 </UL>
48
49 <b>Limitations:</b><br>
50 The following features are not supported.
51 <ul>
52     <li> Ouput in any color formats other than 24-bit RGB or 32-bit RGBA</li>
53     <li> Interlacing </li>
54     <li> The encoder does not understand the following chunk types:
55         <ul>
56             <li>PLTE (color palette)</li>
57             <li>cHRM (device independent color info) </li>
58             <li>gAMA (device independent color info) </li>
59             <li>iCCP (device independent color info) </li>
60             <li>sBIT (original number of significant bits) </li>
61             <li>sRGB (device independent color info) </li>
62             <li>pHYs (physical pixel dimensions) </li>
63             <li>sPLT (suggested reduced palette) </li>
64             <li>tIME (last image modification time) </li>
65         </ul>
66     </li>
67 </ul>
68
69 References:
70 $(LINK2 http://members.gamedev.net/lode/projects/LodePNG/, Original lodepng) <br>
71 $(LINK2 http://www.w3.org/TR/PNG/, PNG Specification) <br>
72 $(LINK2 http://www.libpng.org/pub/png/pngsuite.html, PNG Suite: set of test images) <br>
73 $(LINK2 http://optipng.sourceforge.net/, OptiPNG: tool to experimentally optimize png images)
74 ***************************************************************************************************/
75 module lodepng.Encode;
76
77 version (Tango)
78 {
79     import tango.math.Math;
80     import tango.stdc.string;
81 }
82 else
83 {
84     import std.c.string;
85     import std.math;
86 }
87 import lodepng.ZlibCodec;
88 public import lodepng.Common;
89
90 /***************************************************************************************************
91         Encode pixels as a png file
92
93         This function will attempt to convert to 24-bit RGB if it is a lossless operation, otherwise
94         the resulting image will be in the 32-bit RGBA format. The array returned can be written to disk
95         as a png file.
96
97         params:
98             source=the pixels to be encoded
99             info=description of the source pixels
100         throws: PngException
101         returns: png file of the raw pixels provided by source and described by settings.info
102 ***************************************************************************************************/
103 ubyte[] encode(in ubyte[] source, in PngInfo info)
104 {
105     ubyte[] buf;
106     Chunk[] chunks;
107     return _encode(source, Settings(info), buf, chunks);
108 }
109
110 /***************************************************************************************************
111         Encode pixels and / or raw chunks as a png file
112
113         If chunkList contains IDAT chunks, they must be in order and no source pixels should be provided
114
115         params:
116             source=the pixels to be encoded, can be null
117             info=description of the source pixels
118             chunkList=array of chunks to be written
119         throws: PngException
120         returns: png file of the raw pixels provided by source and described by info
121 ***************************************************************************************************/
122 ubyte[] encode(in ubyte[] source, in PngInfo info, ref Chunk[] chunkList)
123 {
124     ubyte[] buf;
125     return _encode(source, Settings(info), buf, chunkList);
126 }
127
128
129 /***************************************************************************************************
130         Encode pixels as a png file
131
132         see_also: Settings
133         params:
134             source=the pixels to be encoded
135             options=description and options needed to encode the png file
136         throws: PngException
137         returns: png file of the raw pixels provided by source and described by options.info
138 ***************************************************************************************************/
139 ubyte[] encode(in ubyte[] source, Settings options)
140 {
141     ubyte[] buf;
142     Chunk[] chunks;
143     return _encode(source, options, buf, chunks);
144 }
145
146 /***************************************************************************************************
147         Encode pixels as a png file
148
149         see_also: Settings
150         params:
151             source=the pixels to be encoded
152             options=description and options needed to encode the png file
153             chunkList=array of chunks to be written
154         throws: PngException
155         returns: png file of the raw pixels provided by source and described by options.info
156 ***************************************************************************************************/
157 ubyte[] encode(in ubyte[] source, Settings options, ref Chunk[] chunkList)
158 {
159     ubyte[] buf;
160     return _encode(source, options, buf, chunkList);
161 }
162
163 /***************************************************************************************************
164     Filter method
165
166         The png specification defines five types of filters. In addition each scanline can have a
167         different filtering method applied (Dynamic). The latter method usually gives the best compression,
168         when a fixed method is preferred paeth gives the best result most of the times.
169
170 ***************************************************************************************************/
171 enum FilterStrategy : ubyte
172 {
173     None = 0, ///
174     Up = 1, ///
175     Sub = 2, ///
176     Average = 3, ///
177     Paeth = 4, ///
178     Dynamic, ///
179 }
180
181 /***************************************************************************************************
182     Zlib compression method
183
184         Which compression scheme works best depends on the type of image. Tools such as optipng can
185         figure this out experimentally. However, RLE seems to give the best results in compression and
186         performance.
187
188 ***************************************************************************************************/
189 enum CompressionStrategy : ubyte
190 {
191
192     Default = 0, ///
193     Filtered = 1, ///
194     RLE = 3, ///
195     None = ubyte.max ///
196 }
197
198 /** Controls all information needed to encode a png file */
199 struct Settings
200 {
201     /** constructor */
202     static Settings opCall()
203     {
204         Settings result;
205         return result;
206     }
207
208     /** ditto */
209     static Settings opCall(PngInfo info, bool autoRemoveAlpha = false)
210     {
211         Settings result;
212         result.info = info;
213         result.autoRemoveAlpha = autoRemoveAlpha;
214         return result;
215     }
216
217     /** ditto */
218     static Settings opCall(PngImage image, bool autoRemoveAlpha = false)
219     {
220         Settings result;
221         result.info.image = image;
222         result.autoRemoveAlpha = autoRemoveAlpha;
223         return result;
224     }
225
226     invariant()
227     {
228         assert(compressionLevel >=0 && compressionLevel <= 9, "invalid zlib compression level");
229         assert(targetColorType == ColorType.Any ||
230                 targetColorType == ColorType.RGB ||
231                 targetColorType == ColorType.RGBA, "colortype is not supported");
232     }
233
234     /** png image information */
235     PngInfo info;
236
237     /***********************************************************************************************
238         The colortype of the target image
239
240             lodepng can only encode in RGB(A) format (yet?)). If the format is set ColorType.Any, RGB or
241             RGBA is chosen depending on whether the source image has an alpha channel.
242     ***********************************************************************************************/
243     ColorType targetColorType = ColorType.Any;
244
245     /***********************************************************************************************
246         Remove alpha channel
247
248             If set to true and the source image has an alpha channel, this will be removed if (and
249             only if) the image is fully opaque or a colorkey can be written. This is considered a
250             lossless operation.
251     ***********************************************************************************************/
252     bool autoRemoveAlpha = false;
253     /** if zlib compression is to be used on text */
254     bool compressText = false;
255     /** zlib compression level, affects memory use. Must be in range 0-9 */
256     ubyte compressionLevel = 6;
257     /** see FilterStrategy */
258     FilterStrategy filterStrategy = FilterStrategy.Dynamic;
259     /** see CompressionStrategy */
260     CompressionStrategy compressionStrategy = CompressionStrategy.RLE;
261 }
262
263 private
264 {
265     ubyte[] _encode( in ubyte[] source, in Settings settings, ref ubyte[] buffer, ref Chunk[] ChunkList)
266     {
267         // TODO: be more sparing with memory here, can at least avoid one array copy
268
269         // find out what colortype of target should be and whether colorkey (tRNS) should be made
270         ColorType destColor = (settings.targetColorType == ColorType.RGB ||
271                                settings.targetColorType == ColorType.RGBA) ? settings.targetColorType :
272                                settings.info.image.colorType;
273 //       colorkey
274         if (source !is null)
275         {
276             if (!(destColor == ColorType.RGB || destColor == ColorType.RGBA))
277                 destColor = (hasAlphaChannel(settings.info.image.colorType)) ? ColorType.RGBA : ColorType.RGB;
278
279             if (settings.autoRemoveAlpha && destColor == ColorType.RGBA && !settings.info.colorKey)
280             {
281                 ubyte[] colorKey;
282                 if (opaqueOrColorkey(source, settings.info, colorKey))
283                 {
284                     if (colorKey.length)
285                         ChunkList ~= Chunk(tRNS, colorKey);
286                     destColor = ColorType.RGB;
287                 }
288             }
289         }
290
291         // properties of image to be written
292         auto image = PngImage(settings.info.image.width, settings.info.image.height, 8, destColor, source is null ? settings.info.image.interlaced : 0);
293         ChunkList ~= Chunk(IHDR, headerData(image));
294
295         if (source !is null)
296         {
297             // convert pixels if necessary
298             ubyte[] pixels;
299             if (settings.info.image.colorType != destColor || settings.info.image.bitDepth != 8)
300                 pixels = convert(source, settings.info, destColor);
301             else
302                 pixels = source;
303             ChunkList ~= Chunk(IDAT,
304                                Encoder.create(settings.compressionStrategy,
305                                               settings.compressionLevel)(filter(pixels, image, settings.filterStrategy)));
306         }
307         if(settings.info.colorKey)
308             ChunkList ~= Chunk(tRNS, [0, cast(ubyte)settings.info.keyR, 0, cast(ubyte)settings.info.keyG, 0, cast(ubyte)settings.info.keyB]);
309         if(settings.info.backgroundColor.length == 3)
310             ChunkList ~= Chunk( bKGD, [
311                                 0, settings.info.backgroundColor[0],
312                                 0, settings.info.backgroundColor[1],
313                                 0, settings.info.backgroundColor[2] ]);
314         else if(settings.info.backgroundColor.length == 6)
315                ChunkList ~= Chunk( bKGD, settings.info.backgroundColor);
316         if (settings.info.text !is null && (settings.info.text.latin1Text.length > 0 || settings.info.text.unicodeText.length > 0))
317             ChunkList ~= chunkifyText(settings);
318         ChunkList.insertSort();
319         ChunkList ~= Chunk(IEND, []);
320
321         // pre-allocate space needed
322         uint pngLength = 8;
323         foreach(chunk; ChunkList)
324             pngLength += chunk.length;
325         buffer.length = pngLength;
326         buffer.length = 0;
327
328         // create and write all data
329         writeSignature(buffer);
330         foreach(chunk; ChunkList)
331             writeChunk(buffer, chunk);
332
333         return buffer;
334     }
335
336     Chunk[] chunkifyText(ref Settings settings)
337     {
338         Chunk[] result;
339         if (settings.compressText)
340         {
341             auto enc = Encoder.create();
342             foreach(ubyte[] keyword, ubyte[] value; settings.info.text)
343                 result ~= Chunk(zTXt, keyword ~ cast(ubyte[])[0, 0] ~ enc(value));
344             foreach(char[] keyword, char[] value; settings.info.text)
345                 result ~= Chunk(iTXt, cast(ubyte[])keyword ~ cast(ubyte[])[0, 1, 0, 0, 0] ~ enc(cast(ubyte[])value));
346         }
347         else
348         {
349             foreach(ubyte[] keyword, ubyte[] value; settings.info.text)
350                 result ~= Chunk(tEXt, keyword ~ cast(ubyte[])[0] ~ value);
351             foreach(char[] keyword, char[] value; settings.info.text)
352                 result ~= Chunk(iTXt, cast(ubyte[])keyword ~ cast(ubyte[])[0, 0, 0, 0, 0] ~ cast(ubyte[])value);
353         }
354         return result;
355     }
356
357     ubyte[] filter(in ubyte[] source, ref PngImage image, FilterStrategy filterMethod = FilterStrategy.Dynamic)
358     {
359         /* adaptive filtering */
360
361         ubyte[] buffer = new ubyte[image.width * (image.bpp / 8) * image.height];
362         uint bytewidth = (image.bpp + 7) / 8;
363
364         uint scanlength = image.width * bytewidth;
365         buffer.length = image.height * (scanlength + 1) + image.height;
366         ubyte[] line, previous;
367         buffer.length = 0;
368         line = source[0..scanlength];
369         ubyte bestFilter = 0;
370
371         uint absSum(ubyte[] array)
372         {
373             uint result = 0;
374             foreach(value; array)
375                 result += abs(cast(int)(cast(byte)value));
376             return result;
377         }
378
379         uint smallest = absSum(filterMap(line, &None, bytewidth));
380
381         void setSmallest(uint sum, ubyte filterType)
382         {
383             if (sum < smallest)
384             {
385                 smallest = sum;
386                 bestFilter = filterType;
387             }
388         }
389
390         if (filterMethod == FilterStrategy.Dynamic)
391         {
392             for (ubyte f = 1; f < 5; f++)
393                 setSmallest(absSum(dynFilterMap(line, f, bytewidth)), f);
394             buffer ~= bestFilter;
395             buffer ~= dynFilterMap(line, bestFilter, bytewidth);
396
397             for (int y = 1; y < image.height; y++)
398             {
399                 line = source[scanlength * y..scanlength * y + scanlength];
400                 previous = source[scanlength * (y - 1)..scanlength * y];
401
402                 bestFilter = 0;
403                 smallest = absSum(filterMap(line, &None, bytewidth));
404                 for (ubyte f = 1; f < 5; f++)
405                      setSmallest(absSum(dynFilterMap(previous, line, f, bytewidth)), f);
406
407                 buffer ~= bestFilter;
408                 buffer ~= dynFilterMap(previous, line, bestFilter, bytewidth);
409             }
410         }
411         else
412         {
413             buffer ~= cast(ubyte)filterMethod;
414             buffer ~= dynFilterMap(line, cast(ubyte)filterMethod, bytewidth);
415
416             for (int y = 1; y < image.height; y++)
417             {
418                 line = source[scanlength * y..scanlength * y + scanlength];
419                 previous = source[scanlength * (y - 1)..scanlength * y];
420
421                 buffer ~= cast(ubyte)filterMethod;
422                 buffer ~= dynFilterMap(previous, line, cast(ubyte)filterMethod, bytewidth);
423             }
424         }
425         return buffer;
426     }
427
428     bool opaqueOrColorkey(in ubyte[] image, ref PngInfo info, out ubyte[] colorKey)
429     {
430         if (info.image.colorType == ColorType.Greyscale || info.image.colorType == ColorType.RGB)
431             return false;
432         else if (info.image.colorType == ColorType.RGBA )
433         {
434             uint numpixels = info.image.width * info.image.height;
435
436             ubyte[3] ckey;
437             bool hasCkey = false;
438
439             for(size_t i = 0; i < numpixels; i++)
440             {
441                 if(image[i * 4 + 3] != 255)
442                 {
443                     if(image[i * 4 + 3] == 0)
444                     {
445                         if (hasCkey)
446                         {
447                             if (ckey[0] != image[i * 4] || ckey[1] != image[i * 4 + 1] ||
448                                 ckey[2] != image[i * 4 + 2] )
449                                 return false;
450                         }
451                         else
452                         {
453                             hasCkey = true;
454                             ckey[0..2] = image[i * 4 .. i * 4 + 2];
455                         }
456                     }
457                     else
458                         return false;
459                 }
460             }
461             if (hasCkey)
462             {
463                 colorKey = new ubyte[6];
464                 colorKey[1] = ckey[0];
465                 colorKey[3] = ckey[1];
466                 colorKey[5] = ckey[2];
467             }
468             return true;
469         }
470         else if(info.image.colorType == ColorType.GreyscaleAlpha)
471         {
472             size_t numpixels = image.length / 2;
473
474             ubyte[1] ckey;
475             bool hasCkey = false;
476
477             for(size_t i = 0; i < numpixels; i++)
478             {
479                 if(image[i * 2 + 1] != 255)
480                 {
481                     if (hasCkey)
482                     {
483                         if (ckey[0] != image[i * 2])
484                             return false;
485                     }
486                     else
487                     {
488                         hasCkey = true;
489                         ckey[0] = image[i * 2];
490                     }
491                 }
492             }
493             if (hasCkey)
494             {
495                 colorKey = new ubyte[2];
496                 colorKey[1] = ckey[0];
497             }
498             return true;
499         }
500         else if (info.image.colorType == ColorType.Palette)
501         {
502             // TODO: implement this (compression optimization)
503             return false;
504         }
505     }
506
507     ubyte[] headerData(ref PngImage image)
508     {
509         ubyte[] header = new ubyte[13];
510         header.length = 0;
511
512         header.concatUint(image.width);
513         header.concatUint(image.height);
514         header ~= image.bitDepth;
515         header ~= image.colorType;
516         header ~= 0; //compression method
517         header ~= 0; //filter method
518         header ~= image.interlaced; //interlace method
519
520         return header;
521     }
522
523     void concatUint(ref ubyte[] bytestream, uint num)
524     {
525         bytestream.length = bytestream.length + 4;
526         bytestream[$-4] = num >> 24;
527         bytestream[$-3] = num >> 16;
528         bytestream[$-2] = num >> 8;
529         bytestream[$-1] = num;
530     }
531
532     void writeChunk(ref ubyte[] bytestream, ref Chunk chunk)
533     {
534         bytestream.concatUint(chunk.data.length);
535
536         bytestream.length = bytestream.length + 4;
537         bytestream[$-4] = (chunk.type & 0xff000000) >> 24 ;
538         bytestream[$-3] = (chunk.type & 0x00ff0000) >> 16;
539         bytestream[$-2] = (chunk.type & 0x0000ff00) >> 8;
540         bytestream[$-1] =  chunk.type & 0x000000ff;
541
542         bytestream.length = bytestream.length + chunk.data.length;
543         bytestream[$ - chunk.data.length..$] = chunk.data;
544
545         uint CRC = createCRC(bytestream[$ - chunk.data.length - 4.. $]);
546         bytestream.concatUint(CRC);
547     }
548
549     void writeSignature(ref ubyte[] byteStream)
550     {
551         byteStream ~= [137, 80, 78, 71, 13, 10, 26, 10];
552     }
553
554     /++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
555         From the png spec: pixels for filtering are defined as follows such, where x is the current
556         being filtered and c, b correspond to the previous scanline:
557             c b
558             a x
559
560     filters:    construction                                                    Reconstruction
561     0   None    Filt(x) = Orig(x)                                               Recon(x) = Filt(x)
562     1   Sub     Filt(x) = Orig(x) - Orig(a)                                     Recon(x) = Filt(x) + Recon(a)
563     2   Up      Filt(x) = Orig(x) - Orig(b)                                     Recon(x)