1 /++
2 Contains the functions to read/write to hidden data in png files.
3 +/
4 module pngtext.pngtext;
5 
6 import arsd.png;
7 import arsd.color;
8 import utils.misc;
9 import std.file;
10 import std.conv : to;
11 debug{import std.stdio;}
12 
13 // constants
14 
15 /// version
16 private enum PNGTEXT_VERSION = "1.0.1";
17 
18 /// Number of bytes per pixel (red, green, blue, & alpha = 4)
19 private enum BYTES_PER_PIXEL = 4;
20 /// Number of bytes per pixel that are to be used
21 private enum BYTES_USE_PER_PIXEL = 3;
22 /// Number of bytes taken by header in image (after encoding)
23 private enum HEADER_BYTES = 12;
24 /// Density for writing/reading header
25 private enum HEADER_DENSITY = 2;
26 /// length of data stored in header
27 private enum HEADER_LENGTH = HEADER_BYTES * HEADER_DENSITY / 8;
28 /// max number that can be stored in header
29 private enum HEADER_MAX = (1 << HEADER_LENGTH * 8) - 1;
30 
31 version (app){
32 	/// description of constants
33 	public enum CONST_INFO = 
34 "PNGText version: "~PNGTEXT_VERSION~"
35 bytes per pixel: "~BYTES_USE_PER_PIXEL.to!string~"
36 bytes for header: "~HEADER_BYTES.to!string~"
37 bits/byte (density) for header: "~HEADER_DENSITY.to!string~"
38 header length: "~HEADER_LENGTH.to!string~"
39 max stored data length: "~HEADER_MAX.to!string~" bytes";
40 }
41 
42 /// Low storage density (1 bit per 8 bits)
43 public enum DENSITY_LOW = 1;
44 /// Medium storage density (2 bits per 8 bits)
45 public enum DENSITY_MEDIUM = 2;
46 /// High storage density (4 bits per 8 bits)
47 public enum DENSITY_HIGH = 4;
48 /// Maximum storage density (8 bits per 8 bits)
49 public enum DENSITY_MAX = 8;
50 
51 /// Possible values for density
52 public const ubyte[4] DENSITIES = [1, 2, 4, 8];
53 /// names associated with above densities
54 public const string[4] DENSITY_NAMES = ["low", "medium", "high", "maximum"];
55 /// quality names associated with above densities
56 public const string[4] QUALITY_NAMES = ["highest", "medium", "low", "zero"];
57 
58 /// Bytes with the number of bits as 1 same as density
59 private const ubyte[4] DENSITY_BYTES = [0B00000001, 0B00000011, 0B00001111, 0B11111111];
60 
61 /// To read/write data from a png image
62 public class PNGText{
63 private:
64 	/// the png image currently being edited
65 	TrueColorImage _pngImage;
66 	/// the png image as an array of ubyte (excluding alpha)
67 	ubyte[] _stream;
68 	/// filename of currently loaded image
69 	string _filename;
70 	/// if there is an image loaded
71 	bool _loaded = false;
72 	/// max number of bytes currently loaded image can store. -1 if not yet calculated
73 	int[ubyte] _capacity;
74 	/// the data to encode into the image
75 	ubyte[] _data;
76 
77 	/// "splits" a single ubyte over an array of ubyte
78 	/// 
79 	/// `val` is the byte to split
80 	/// `densityMask` is the index of chosen density in `DENSITIES`
81 	/// `r` is the array/slice to put the splitted byte to
82 	static void splitByte(ubyte val, ubyte densityIndex, ubyte[] r){
83 		immutable ubyte mask = DENSITY_BYTES[densityIndex];
84 		immutable ubyte density = DENSITIES[densityIndex];
85 		for (ubyte i = 0; i < r.length; i ++){
86 			r[i] = (r[i] & (~cast(int)mask) ) | ( ( val >> (i * density) ) & mask );
87 		}
88 	}
89 	/// opposite of splitByte, joins last bits from ubyte[] to make a single ubyte
90 	static ubyte joinByte(ubyte[] bytes, ubyte densityIndex){
91 		immutable ubyte mask = DENSITY_BYTES[densityIndex];
92 		immutable ubyte density = DENSITIES[densityIndex];
93 		ubyte r = 0;
94 		for (ubyte i = 0; i < bytes.length; i ++){
95 			r |= ( bytes[i] & mask ) << (i * density);
96 		}
97 		return r;
98 	}
99 
100 	/// Calculates capacity of an image
101 	void calculateCapacity(ubyte density){
102 		if (!_loaded || _stream.length <= HEADER_BYTES)
103 			return;
104 		_capacity[density] = ((cast(int)_stream.length - HEADER_BYTES) * density) / 8; /// bytes * density / 8;
105 	}
106 	/// ditto
107 	void calculateCapacity(){
108 		foreach (density; DENSITIES)
109 			calculateCapacity(density);
110 	}
111 	/// Calculates number of bytes needed in _stream to store n bytes of data. Adjusts for HEADER_BYTES
112 	///
113 	/// Returns: number of bytes needed
114 	static int streamBytesNeeded(int n, ubyte density){
115 		if (n == 0)
116 			return HEADER_BYTES;
117 		return HEADER_BYTES + ((n * 8)/density);
118 	}
119 	/// Calculates smallest value for density with which n bytes of data will fit in a number of bytes of _stream
120 	/// 
121 	/// Returns: smallest value for density that can be used in this case, or 0 if data wont fit
122 	static ubyte calculateOptimumDensity(int n, int streamBytes){
123 		foreach (density; DENSITIES){
124 			if (streamBytesNeeded(n, density) <= streamBytes)
125 				return density;
126 		}
127 		return 0;
128 	}
129 	/// reads header bytes from stream
130 	ubyte[HEADER_LENGTH] readHeader(){
131 		ubyte[HEADER_LENGTH] header;
132 		immutable ubyte byteCount = 8 / HEADER_DENSITY; /// number of bytes in stream for single byte of header
133 		immutable ubyte densityIndex = cast(ubyte)DENSITIES.indexOf(HEADER_DENSITY);
134 		foreach (i; 0 .. HEADER_LENGTH){
135 			header[i] = joinByte(_stream[i*byteCount .. (i+1)*byteCount], densityIndex);
136 		}
137 		return header;
138 	}
139 	/// writes bytes to header in stream
140 	void writeHeader(ubyte[] header){
141 		immutable ubyte byteCount = 8 / HEADER_DENSITY; /// number of bytes in stream for single byte of header
142 		immutable ubyte densityIndex = cast(ubyte)DENSITIES.indexOf(HEADER_DENSITY);
143 		if (header.length > HEADER_LENGTH)
144 			header = header[0 .. HEADER_LENGTH];
145 		foreach (i, byteVal; header){
146 			splitByte(byteVal, densityIndex, _stream[i*byteCount .. (i+1)*byteCount]);
147 		}
148 	}
149 	/// encodes _data to _stream. No checks are performed, so make sure the data fits before calling this
150 	void encodeDataToStream(ubyte density, int offset = HEADER_BYTES){
151 		immutable ubyte densityIndex = cast(ubyte)DENSITIES.indexOf(density);
152 		immutable ubyte byteCount = 8 / density;
153 		foreach (i, byteVal; _data){
154 			splitByte(byteVal, densityIndex, _stream[(i*byteCount) + offset .. ((i+1)*byteCount) + offset]);
155 		}
156 	}
157 	/// reads into _data from _stream. _data must have enough length before this function is called
158 	void decodeDataFromStream(ubyte density, int length, int offset = HEADER_BYTES){
159 		immutable ubyte densityIndex = cast(ubyte)DENSITIES.indexOf(density);
160 		immutable ubyte byteCount = 8 / density;
161 		foreach (i; 0 .. length){
162 			_data[i] = joinByte(_stream[(i*byteCount) + offset .. ((i+1)*byteCount) + offset], densityIndex);
163 		}
164 	}
165 	/// writes _stream back into _pngImage
166 	void encodeStreamToImage(){
167 		for (int i = 0, readIndex = 0; i < _pngImage.imageData.bytes.length && readIndex < _stream.length;
168 		i += BYTES_PER_PIXEL){
169 			foreach (j; 0 .. 3){
170 				if (readIndex >= _stream.length)
171 					break;
172 				_pngImage.imageData.bytes[i+j] = _stream[readIndex];
173 				readIndex ++;
174 			}
175 		}
176 	}
177 	/// Creates a dummy stream of length l. USE FOR DEBUG ONLY
178 	void createDummyStream(uint l){
179 		_stream.length = l;
180 	}
181 public:
182 	/// Constructor
183 	this(){
184 		_loaded = false;
185 	}
186 	~this(){
187 	}
188 	/// Checks if a number is a valid value for use as storage density
189 	/// 
190 	/// Returns: true if valid
191 	static bool isValidDensity(ubyte density){
192 		return DENSITIES.hasElement(density);
193 	}
194 	/// Returns: a string describing a certain density. Or an empty string in case of invalid density value
195 	static string densityName(ubyte density){
196 		if (!isValidDensity(density))
197 			return "";
198 		return DENSITY_NAMES[DENSITIES.indexOf(density)];
199 	}
200 	/// Returns: a string describing image quality at a certain density. or empty string in case of invalid density value
201 	static string qualityName(ubyte density){
202 		if (!isValidDensity(density))
203 			return "";
204 		return QUALITY_NAMES[DENSITIES.indexOf(density)];
205 	}
206 	/// Returns: the filename to read/write the image from/to
207 	@property string filename(){
208 		return _filename;
209 	}
210 	/// ditto
211 	@property string filename(string newVal){
212 		return _filename = newVal;
213 	}
214 	/// Returns: true if an image is loaded
215 	@property bool imageLoaded(){
216 		return _loaded;
217 	}
218 	/// Returns: the data to encode, or the data that was decoded from a loaded image
219 	@property ref ubyte[] data(){
220 		return _data;
221 	}
222 	/// ditto
223 	@property ref ubyte[] data(ubyte[] newVal){
224 		return _data = newVal;
225 	}
226 	/// Returns: the capacity of an image at a certain storage density. In case of error, like invalid density value, returns -1
227 	int capacity(ubyte density){
228 		if (!isValidDensity(density))
229 			return -1;
230 		if (density !in _capacity || _capacity[density] == -1)
231 			calculateCapacity(density);
232 		return _capacity[density];
233 	}
234 	/// Loads an image.
235 	/// 
236 	/// Throws: Exception in case of error
237 	void load(){
238 		if (_filename == "" || !exists(_filename) || !isFile(_filename))
239 			throw new Exception(_filename~" is not a valid filename, or file does not exist");
240 		_loaded = false;
241 		_pngImage = readPng(_filename).getAsTrueColorImage;
242 		immutable int height = _pngImage.height, width = _pngImage.width;
243 		_stream.length = height * width * BYTES_USE_PER_PIXEL;
244 		for (int i = 0, writeIndex = 0; i < _pngImage.imageData.bytes.length; i += BYTES_PER_PIXEL){
245 			foreach (j; 0 .. 3){
246 				_stream[writeIndex] = _pngImage.imageData.bytes[i+j];
247 				writeIndex ++;
248 			}
249 		}
250 		_loaded = true;
251 	}
252 	/// Saves an image
253 	/// 
254 	/// Throws: Exception in case of error
255 	void save(){
256 		if (_filename == "")
257 			throw new Exception(_filename~" is not a valid filename, or file already exists");
258 		writePng(_filename, _pngImage);
259 	}
260 	/// Calculates the least density that can be used to store n bytes into loaded image.
261 	/// 
262 	/// Returns: calculated density, or zero in case of error
263 	ubyte calculateOptimumDensity(int n){
264 		if (!imageLoaded || n <= HEADER_BYTES)
265 			return 0;
266 		return calculateOptimumDensity(n, cast(int)_stream.length);
267 	}
268 	/// encodes data into loaded image.
269 	/// 
270 	/// Throws: Exception on error
271 	void encode(){
272 		if (!imageLoaded)
273 			throw new Exception("no image loaded, cannot encode data");
274 		immutable ubyte density = calculateOptimumDensity(cast(int)_data.length);
275 		if (density == 0)
276 			throw new Exception("image too small to hold data");
277 		if (_data.length > HEADER_MAX)
278 			throw new Exception("data is bigger than "~HEADER_MAX.to!string~" bytes");
279 		// put header in, well, header
280 		ubyte[HEADER_LENGTH] header;
281 		foreach (i; 0 .. HEADER_LENGTH)
282 			header[i] = cast(ubyte)( _data.length >> (i * 8) );
283 		this.writeHeader(header);
284 		this.encodeDataToStream(density);
285 		this.encodeStreamToImage();
286 	}
287 	/// decodes data from loaded image
288 	/// 
289 	/// Throws: Exception on error
290 	void decode(){
291 		if (!imageLoaded)
292 			throw new Exception("no image loaded, cannot decode data");
293 		// read header, needed for further tests
294 		if (_stream.length < HEADER_BYTES)
295 			throw new Exception("image too small to hold data, invalid header");
296 		immutable ubyte[HEADER_LENGTH] header = readHeader();
297 		int len = 0;
298 		foreach (i, byteVal; header)
299 			len |= byteVal << (i * 8);
300 		immutable ubyte density = calculateOptimumDensity(len);
301 		if (len > HEADER_MAX || len > capacity(DENSITY_MAX) || density == 0)
302 			throw new Exception("invalid data");
303 		_data.length = len;
304 		decodeDataFromStream(density, len);
305 	}
306 }
307 /// 
308 unittest{
309 	writeln("pngtext.d unittests:");
310 	import std.stdio : writeln;
311 	import std.conv : to;
312 	writeln("unittests for PNGText.splitByte and PNGText.joinByte started");
313 	// splitByte
314 	ubyte[] bytes;
315 	bytes.length = 8;
316 	bytes[] = 128;
317 	PNGText.splitByte(cast(ubyte)0B10101010, cast(ubyte)0, bytes);
318 	assert(bytes == [128, 129, 128, 129, 128, 129, 128, 129]);
319 	assert(PNGText.joinByte(bytes, cast(ubyte)0) == 0B10101010);
320 
321 	PNGText.splitByte(cast(ubyte)0B10101010, cast(ubyte)1, bytes);
322 	assert(bytes[0 .. 4] == [130, 130, 130, 130]);
323 	assert(PNGText.joinByte(bytes[0 .. 4], cast(ubyte)1) == 0B10101010);
324 
325 	PNGText.splitByte(cast(ubyte)0B10101010, cast(ubyte)2, bytes);
326 	assert(bytes[0 .. 2] == [0B10001010, 0B10001010]);
327 	assert(PNGText.joinByte(bytes[0 .. 2], cast(ubyte)2) == 0B10101010);
328 
329 	PNGText.splitByte(cast(ubyte)0B10101010, cast(ubyte)3, bytes);
330 	assert(bytes[0] == 0B10101010);
331 	assert(PNGText.joinByte(bytes[0 .. 1], cast(ubyte)3) == 0B10101010);
332 
333 	writeln("unittests for PNGText.writeHeader and PNGText.readHeader started");
334 	// write some stuff
335 	PNGText obj = new PNGText();
336 	obj.createDummyStream(1024);
337 	obj._stream[] = 0B01011100;
338 	bytes.length = HEADER_LENGTH;
339 	bytes = [215, 1, 0];
340 	obj.writeHeader(bytes);
341 	assert (obj.readHeader == bytes);
342 	// write some more stuff
343 	bytes[] = 0B00111100;
344 	obj.writeHeader(bytes);
345 	assert(obj.readHeader == [0B00111100, 0B00111100, 0B00111100]);
346 
347 	writeln("unittests for PNGText.encodeDataToStream and PNGText.decodeDataFromStream started");
348 	bytes.length = 100;
349 	foreach (i; 0 .. bytes.length){
350 		bytes[i] = cast(ubyte)i;
351 	}
352 	foreach (density; DENSITIES){
353 		obj._data = bytes.dup;
354 		writeln("\tencoding with density=",density);
355 		obj.encodeDataToStream(density);
356 		obj._data[] = 0;
357 		writeln("\tdecoding with density=",density);
358 		obj.decodeDataFromStream(density, 100);
359 		assert(obj._data == bytes);
360 	}
361 	writeln("pngtext.d unittests over");
362 }