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