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 }