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 }