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 }