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 utils.lists;
10 import utils.baseconv;
11 import std.math;
12 import std.file;
13 debug{import std.stdio;}
14 
15 // constants
16 
17 /// Number of bytes per pixel (red, green, blue, & alpha = 4)
18 const BYTES_PER_PIXEL = 4;
19 /// Number of bytes taken by header
20 const HEADER_BYTES = 12;
21 /// Density for writing/reading header
22 const HEADER_DENSITY = 2;
23 
24 /// reads a png as a stream of ubyte[4]
25 /// no checks are present in here to see if pngFilename is valid or not
26 /// Returns: array with values of rgba of all pixels one after another
27 private ubyte[] readAsStream(string pngFilename, ref uinteger width, ref uinteger height){
28 	MemoryImage pngMem = readPng(pngFilename);
29 	ubyte[] r;
30 	height = pngMem.height;
31 	width = pngMem.width;
32 	r.length = height * width * BYTES_PER_PIXEL;
33 	for (uinteger i = 0, readTill = height * width; i < readTill; i ++){
34 		Color pixel = pngMem.getPixel(cast(int)(i % width), cast(int)(i / width));
35 		r[i*BYTES_PER_PIXEL .. (i+1)*BYTES_PER_PIXEL] = [pixel.r, pixel.g, pixel.b, pixel.a];
36 	}
37 	return r;
38 }
39 
40 /// writes a png from a stream of ubyte[4] to a file
41 /// no checks are present in here to see if pngFilename is valid or not
42 private void savePngStream(ubyte[] stream, string pngFilename, uinteger width, uinteger height){
43 	/// change to TrueColorImage
44 	TrueColorImage pngMem = new TrueColorImage(cast(int)width, cast(int)height);
45 	auto colorData = pngMem.imageData.colors;
46 	for (uinteger i = 0; i < stream.length; i += BYTES_PER_PIXEL){
47 		colorData[i/BYTES_PER_PIXEL] = Color(stream[i], stream[i+1], stream[i+2], stream[i+3]);
48 	}
49 	writePng(pngFilename, pngMem);
50 }
51 
52 /// Returns: how many bytes a png can store
53 public uinteger calculatePngCapacity(string pngFilename, ubyte density){
54 	MemoryImage pngMem = readPng(pngFilename);
55 	uinteger pixelCount = pngMem.width * pngMem.height;
56 	if (pixelCount <= 3){
57 		return 0;
58 	}
59 	return (pixelCount - 3)*density/2;
60 }
61 
62 /// Returns: bytes required to hold data at a certain density
63 private uinteger calculateBytesNeeded(uinteger dataLength, ubyte density){
64 	return dataLength * 8 / density;
65 }
66 
67 /// writes some data to a png image
68 /// Returns: [] if no errors occurred, or array of strings containing errors
69 public string[] writeDataToPng(string pngFilename, string outputFilename, ubyte[] data){
70 	uinteger width, height;
71 	string[] errors = [];
72 	if (!exists(pngFilename) || !isFile(pngFilename)){
73 		errors ~= "file does not exist";
74 	}else{
75 		ubyte[] pngStream = readAsStream(pngFilename, width, height);
76 		try{
77 			pngStream = encodeDataToPngStream(pngStream, data.dup);
78 			savePngStream(pngStream, outputFilename, width, height);
79 		}catch (Exception e){
80 			errors ~= e.msg;
81 		}
82 	}
83 	return errors;
84 }
85 
86 /// reads some data from a png image
87 /// Returns: the data read in a string
88 /// Throws: Exception in case of error
89 public ubyte[] readDataFromPng(string pngFilename){
90 	if (!exists(pngFilename) || !isFile(pngFilename)){
91 		throw new Exception ("file does not exist");
92 	}
93 	uinteger w, h;
94 	ubyte[] pngStream = readAsStream(pngFilename, w, h);
95 	return extractDataFromPngStream(pngStream);
96 }
97 
98 /// reads the header (data-length) from begining of png stream
99 private uinteger readHeader(ubyte[] stream){
100 	ubyte[HEADER_BYTES / BYTES_PER_PIXEL] headerBytes;
101 	ubyte[HEADER_BYTES] headerPixels;
102 	foreach (i, b; stream[0 .. HEADER_BYTES]){
103 		headerPixels[i] = b.readLastBits(HEADER_DENSITY);
104 	}
105 	ubyte bytesPerChar = 8 / HEADER_DENSITY;
106 	for (uinteger i = 0; i < HEADER_BYTES; i += bytesPerChar){
107 		headerBytes[i / bytesPerChar] = joinByte(headerPixels[i .. i + bytesPerChar]);
108 	}
109 	return charToDenary(cast(char[])headerBytes);
110 }
111 
112 /// Returns: the header (first 3 pixels storing the data-length)
113 private ubyte[HEADER_BYTES] writeHeader(uint dataLength, ubyte[HEADER_BYTES] stream){
114 	assert (dataLength <= pow (2, 24), "data-length must be less than 16 megabytes");
115 	ubyte[] data = cast(ubyte[])(dataLength.denaryToChar());
116 	ubyte[] rawData;
117 	ubyte bytesPerChar = 8/HEADER_DENSITY;
118 	for (uinteger i = 0; i < data.length; i ++){
119 		rawData ~= data[i].splitByte(bytesPerChar);
120 	}
121 	if (rawData.length < HEADER_BYTES){
122 		ubyte[] newData;
123 		newData.length = HEADER_BYTES;
124 		newData[] = 0;
125 		newData[HEADER_BYTES - rawData.length .. newData.length] = rawData;
126 		rawData = newData;
127 	}
128 	stream = stream.dup;
129 	for (uinteger i = 0; i < rawData.length; i ++){
130 		stream[i] = stream[i].setLastBits(HEADER_DENSITY, rawData[i]);
131 	}
132 	return stream;
133 }
134 
135 /// Returns: the optimum density (densities for a certain range), so all data can fit, while the highest quality is kept.
136 /// 
137 /// the return is assoc_array, where the index is the density, and the value at index is the length of bytes on which that density 
138 /// should be applied
139 /// 
140 /// only works properly if the dataLength with density 8 can at least fit into the streamLength
141 private uinteger[ubyte] calculateOptimumDensity(uinteger streamLength, uinteger dataLength){
142 	const ubyte[4] possibleDensities = [1,2,4,8];
143 	// look for the max density required. i.e, check (starting from lowest) each density, and see with which one the data fits in
144 	ubyte maxDensity = 1;
145 	foreach (i, currentDensity; possibleDensities){
146 		uinteger requiredLength = calculateBytesNeeded(dataLength, currentDensity);
147 		if (requiredLength == streamLength){
148 			// fits exactly, return just this
149 			return [currentDensity : dataLength];
150 		}else if (requiredLength < streamLength){
151 			// some bytes are left, better decrease density and use thme too, 
152 			maxDensity = currentDensity;
153 			break;
154 		}
155 	}
156 	// if at maxDensity, the data starts to fit, but some bytes are left, then the density before maxDensity could be used for some 
157 	// bytes
158 	ubyte minDensity = maxDensity == possibleDensities[0] ? maxDensity : possibleDensities[possibleDensities.indexOf(maxDensity)-1];
159 	if (minDensity == maxDensity){
160 		return [maxDensity : dataLength];
161 	}
162 	// now calculate how many bytes for maxDensity, how many for minDensity
163 	uinteger minDensityBytesCount = 1;
164 	for (; minDensityBytesCount < dataLength; minDensityBytesCount ++){
165 		if (calculateBytesNeeded(minDensityBytesCount, minDensity) + calculateBytesNeeded(dataLength-minDensityBytesCount, maxDensity)
166 			> streamLength){
167 				// the last value should work
168 				minDensityBytesCount --;
169 				return [
170 					minDensity : minDensityBytesCount,
171 					maxDensity : dataLength-minDensityBytesCount
172 				];
173 		}
174 	}
175 	// if nothing worked till now, just give up and use the max density
176 	return [maxDensity : dataLength];
177 }
178 
179 /// ditto
180 public uinteger[ubyte] calculateOptimumDensity(string filename, uinteger dataLength){
181 	MemoryImage memPng = readPng(filename);
182 	uinteger streamLength = (memPng.width * memPng.height * BYTES_PER_PIXEL) - (HEADER_BYTES / BYTES_PER_PIXEL);
183 	return calculateOptimumDensity(streamLength,dataLength);
184 }
185 
186 /// extracts the stored data-stream from a png-stream
187 /// Returns: the stream representing the data
188 private ubyte[] extractDataFromPngStream(ubyte[] stream){
189 	// stream.length must be at least 3 pixels, i.e stream.length == 3*4
190 	assert (stream.length >= HEADER_BYTES, "image does not have enough pixels");
191 	stream = stream.dup;
192 	uinteger length = readHeader(stream[0 .. HEADER_BYTES]);
193 	stream = stream[HEADER_BYTES .. stream.length];
194 	// calculate the required density
195 	uinteger[ubyte] densities = calculateOptimumDensity(stream.length, length);
196 	// extract the data
197 	ubyte[] data = [];
198 	uinteger readFrom = 0; /// stores from where the next reading will start from
199 	foreach (density, dLength; densities){
200 		ubyte bytesPerChar = 8 / density;
201 		uinteger readTill = readFrom + (dLength * bytesPerChar);
202 		// make sure it does not exceed the stream
203 		if (readTill > stream.length){
204 			// return what was read
205 			return data;
206 		}
207 		data = data ~ stream[readFrom .. readTill].readLastBits(density);
208 		readFrom = readTill;
209 	}
210 	return data;
211 }
212 
213 private ubyte[] encodeDataToPngStream(ubyte[] stream, ubyte[] data){
214 	// stream.length must be at least 3 pixels, i.e stream.length == 3*4
215 	assert (stream.length >= HEADER_BYTES, "image does not have enough pixels");
216 	// data can not be more than or equal to 2^(4*3*2) = 2^24 bytes
217 	assert (data.length < pow(2, HEADER_BYTES * HEADER_DENSITY), "data length is too much to be stored in header");
218 	// make sure it'll fit, using the max density
219 	assert (calculateBytesNeeded(data.length, 8) + HEADER_BYTES <= stream.length,
220 		"image does not have enough pixels to hold that mcuh data");
221 	stream = stream.dup;
222 	// put the header into the data (header = stores the length of the data, excluding the header)
223 	ubyte[HEADER_BYTES] headerStream = writeHeader(cast(uint)data.length, stream[0 .. HEADER_BYTES]);
224 	stream = stream[HEADER_BYTES .. stream.length];
225 	// now deal with the data
226 	uinteger[ubyte] densities = calculateOptimumDensity(stream.length, data.length);
227 	uinteger readFromData = 0;
228 	uinteger readFromStream = 0;
229 	foreach (density, dLength; densities){
230 		ubyte bytesPerChar = 8 / density;
231 		// split it first
232 		ubyte[] raw;
233 		raw.length = dLength * bytesPerChar;
234 		for (uinteger i = 0; i < dLength; i ++){
235 			raw[i*bytesPerChar .. (i+1)*bytesPerChar] = splitByte(data[readFromData + i], bytesPerChar);
236 		}
237 		readFromData += dLength;
238 		// now merge it into the stream
239 		stream[readFromStream .. readFromStream + (dLength * bytesPerChar)] = 
240 			stream[readFromStream .. readFromStream + (dLength * bytesPerChar)].setLastBits(density, raw);
241 		readFromStream += dLength * bytesPerChar;
242 	}
243 	return headerStream ~ stream;
244 }
245 
246 /// stores a number in the last n-bits of a ubyte, the number must be less than 2^n
247 private ubyte setLastBits(ubyte originalNumber, ubyte n, ubyte toInsert){
248 	assert (n > 0 && toInsert < pow(2, n), "n must be > 0 and toInsert < pow(2,n) in setLastBits");
249 	// first, empty the last bits, so we can just use + to add
250 	originalNumber -=  originalNumber % pow(2, n);
251 	return cast(ubyte)(originalNumber + toInsert);
252 }
253 /// unittest
254 unittest{
255 	assert (cast(ubyte)(255).setLastBits(2,1) == 253);
256 	assert (cast(ubyte)(3).setLastBits(2,0) == 0);
257 	assert (cast(ubyte)(8).setLastBits(4,7) == 7);
258 }
259 
260 /// stores numbers in the last n-bits of ubytes, each of the numbers must be less than 2^n
261 private ubyte[] setLastBits(ubyte[] originalNumbers, ubyte n, ubyte[] toInsert){
262 	assert (toInsert.length <= originalNumbers.length, "toInsert.length must be <= originalNumbers.length");
263 	ubyte[] r;
264 	r.length = toInsert.length;
265 	for (uinteger i = 0; i < toInsert.length; i ++){
266 		r[i] = setLastBits(originalNumbers[i], n, toInsert[i]);
267 	}
268 	return r;
269 }
270 /// 
271 unittest{
272 	assert ([255,3,2].setLastBits(2, [1,1,0]) == [254, 3, 0]);
273 }
274 
275 /// reads and returns the number stored in last n-bits of a ubyte
276 private ubyte readLastBits(ubyte orignalNumber, ubyte n){
277 	return cast(ubyte)(orignalNumber % pow(2, n));
278 }
279 /// unittest
280 unittest{
281 	assert (cast(ubyte)(255).readLastBits(2) == 3);
282 	assert (cast(ubyte)(3).readLastBits(2) == 3);
283 	assert (cast(ubyte)(9).readLastBits(3) == 1);
284 }
285 
286 /// reads and returns the numbers stored in the last n-birs of ubytes. It also joins the numbers so instead of n-bit numbers, they
287 /// are 8 bit numbers
288 private ubyte[] readLastBits(ubyte[] originalNumbers, ubyte n){
289 	ubyte[] rawData;
290 	rawData.length = originalNumbers.length;
291 	for (uinteger i = 0; i < rawData.length; i++){
292 		rawData[i] = originalNumbers[i].readLastBits(n);
293 	}
294 	// join them into single byte
295 	ubyte bytesPerChar = 8/n;
296 	rawData.length -= rawData.length % bytesPerChar;
297 	ubyte[] data;
298 	data.length = rawData.length/bytesPerChar;
299 	for (uinteger i = 0; i < rawData.length; i += bytesPerChar){
300 		data[i / bytesPerChar] = joinByte(rawData[i .. i + bytesPerChar]);
301 	}
302 	return data;
303 }
304 /// 
305 unittest{
306 	assert ([255,3,2].readLastBits(2) == [3,3,2]);
307 }
308 
309 /// splits a number stored in ubyte into several bytes
310 /// number is the number to split
311 /// n is the number of bytes to split into
312 private ubyte[] splitByte(ubyte number, ubyte n){
313 	ubyte[] r;
314 	r.length = n;
315 	uint modBy = pow(2, 8 / n);
316 	for (uinteger i = 0; i < n; i ++){
317 		r[i] = number % modBy;
318 		number = number / modBy;
319 	}
320 	return r;
321 }
322 /// unittest
323 unittest{
324 	assert (255.splitByte(2) == [15,15]);
325 	assert (255.splitByte(4) == [3,3,3,3]);
326 	assert (127.splitByte(2) == [15,7]);
327 }
328 
329 /// joins bits from multiple bytes into a single number, i.e: opposite of splitByte
330 /// split is the ubyte[] in which only the last n-bits from each byte stores the required number
331 /// split.length must be either 2, 4, or 8
332 private ubyte joinByte(ubyte[] split){
333 	assert (split.length == 1 || split.length == 2 || split.length == 4 || split.length == 8,
334 		"split.length must be either 1, 2, 4, or 8");
335 	ubyte r = 0;
336 	ubyte bitCount = 8 / split.length;
337 	uint modBy = cast(uint)pow(2, bitCount);
338 	for (uinteger i = 0; i < split.length; i ++){
339 		r += (split[i] % modBy) * pow(2, i * bitCount);
340 	}
341 	return r;
342 }
343 /// unittest
344 unittest{
345 	assert (255.splitByte(4).joinByte == 255);
346 	assert (255.splitByte(2).joinByte == 255);
347 	assert (126.splitByte(4).joinByte == 126);
348 	assert (127.splitByte(4).joinByte == 127);
349 }