in reply to Cannot Set Image Pixel to a Random Colour using GD

I am writing a graphics lib entirely written in Perl! Here is what it can do so far: Read any bmp photo. Change saturation. Save As: BMP in any truecolor format. It will work on any computer that has Perl, because it does not use ImageMagick or any kind of magic.

You're welcome.

#!/usr/bin/perl use 5.004; use strict; use warnings; $| = 1; # Stop buffering stdout. my $CANVAS = ReadBMP('photo.bmp'); Saturation($CANVAS, 50); # Reduce saturation to 50% of original SaveBMP($CANVAS, 'photo2.bmp', 1); exit; # This function adjusts the saturation of an image # by a percentage: # 0 = Grayscale # 50 = 50% less saturation # 100 = ORIGINAL IMAGE # 150 = 50% greater saturation # 200 = 100% greater saturation # Usage: Saturation(CANVASREF, PERCENT) sub Saturation { defined $_[1] or return 0; my ($CANVAS, $W, $H, $D, $PTR) = UseCanvas($_[0]) or return 0; my $PERCENT = $_[1]; if ($PERCENT == 100) { return 0; } my $S = "\nSaturation():"; unless ($D == 1 || $D == 3 || $D == 4) { GPRINT("$S Cannot work on ", ($D << 3), '-bit image!', "$S 8-bit, 24-bit, or 32-bit image required!\n"); return 0; } my $RES = $W * $H; GPRINT("$S Adjusting $W x $H image saturation by ", int($PERCENT * 1000) / 10, " % ..."); my ($R, $G, $B); $PERCENT /= 100; while ($RES--) { # Read pixel: if ($D == 1) { $R = $G = $B = vec($$CANVAS, $PTR, 8); } else { if ($D == 4) { $PTR++; } $R = vec($$CANVAS, $PTR, 8); $G = vec($$CANVAS, $PTR + 1, 8); $B = vec($$CANVAS, $PTR + 2, 8); } # Here we calculate the grayscale equivalent of each pixel. # Then we compare the grayscale value to the original and # multiply the difference by a percentage. This will either # reduce or amplify the difference in saturation: my $GRAY = int(($R + $G + $B) / 3 + 0.5); $R = int($GRAY + ($R - $GRAY) * $PERCENT); $G = int($GRAY + ($G - $GRAY) * $PERCENT); $B = int($GRAY + ($B - $GRAY) * $PERCENT); # Check for overflow. $R >= 0 or $R = 0; $R <= 255 or $R = 255; $G >= 0 or $G = 0; $G <= 255 or $G = 255; $B >= 0 or $B = 0; $B <= 255 or $B = 255; # Write pixel if ($D == 1) { vec($$CANVAS, $PTR++, 8) = int(($R + $G + $B) / 3 + 0.5); } else { vec($$CANVAS, $PTR, 8) = $R; vec($$CANVAS, $PTR + 1, 8) = $G; vec($$CANVAS, $PTR + 2, 8) = $B; $PTR += 3; } } GPRINT("DONE.\n"); return 1; } # This function returns a reference to a canvas object # along with its width, height and depth... # Returns an empty list if the canvas object is # missing or corrupt or if it has a different # format than what's requested. # # Usage: ARRAY = UseCanvas(CANVASREF, [REQUESTS]) # # The following values are returned on success: # ARRAY[0] = Reference to the canvas object # ARRAY[1] = Image width in pixels # ARRAY[2] = Image height in pixels # ARRAY[3] = Image depth (bytes per pixel) # ARRAY[4] = Byte Pointer to where pixel data begins # sub UseCanvas { my $REF = GetCanvasRef($_[0]) or return (); my $D = DepthOf($REF); shift; foreach (@_) { if (($_ == 32 || $_ == 4) && $D == 4) { $D = 4; last; } if (($_ == 24 || $_ == 3) && $D == 3) { $D = 3; last; } if (($D == 8) && ($_ == 8 || $_ == 1)) { $D = 1; last; } else { $D = 0; } } $D or return (); # Check canvas size. FixCanvas($REF); my $W = WidthOf($REF); my $H = HeightOf($REF); return ($REF, $W, $H, $D, 16); } # # Canvas | Graphics | v2022.12.13 # This function converts the image depth to # bytes per pixel. It doesn't matter if you provide # the depth in bits per pixel or bytes per pixel or # the number of colors. This function always returns # the value in bytes per pixel. # This function returns the DEFAULT value if an # invalid depth is given. If a DEFAULT value is # not specified, then zero is the default value. # # Usage: BYTES_PER_PIXEL = GetBPP(DEPTH, [DEFAULT]) # sub GetBPP { my $D = IntRange($_[0], 0, 9999999999); if ($D == 1 || $D == 8 || $D == 256) { return 1; } if ($D == 2 || $D == 16 || $D == 65536) { return 2; } if ($D == 3 || $D == 24 || $D == 16777216) { return 3; } if ($D == 4 || $D == 32 || $D == 4294967296) { return 4; } return defined $_[1] ? $_[1] : 0; } # # Canvas | Graphics | v2022.12.13 # This function overwrites the first 16 bytes of a # string with a new canvas header. The first # argument must be a string reference! # # Usage: FixCanvasHeader(CANVASREF, Width, Height, [Depth]) # sub FixCanvasHeader { defined $_[0] && ref($_[0]) eq 'SCALAR' or return 0; my $REF = shift; defined $$REF or $$REF = ''; substr($$REF, 0, 16) = CreateCanvasHeader(@_); return 1; } # This function returns a new 16-byte canvas header # string which starts with the word 'CANVAS' and # contains the canvas width, height, and depth. # # This function will always return a valid canvas # header even if you provide invalid arguments. # # Usage: HEADER = CreateCanvasHeader(Width, Height, Depth) # sub CreateCanvasHeader { my $D = GetBPP($_[2], 3); # Convert image depth to bytes per pixel my $HEADER = sprintf('CANVAS%0.2d', $D << 3); vec($HEADER, 2, 32) = IntRange($_[0], 0, 4294967295); # Save width vec($HEADER, 3, 32) = IntRange($_[1], 0, 4294967295); # Save height return $HEADER; } # Returns the pixel width of the canvas. # No error checking is done, so make sure to provide # the correct argument everytime. # Usage: ImageWidth = WidthOf(CANVASREF) # sub WidthOf { my $REF = $_[0]; return vec($$REF, 2, 32); } # Returns the pixel height of the canvas. # No error checking is done, so make sure to provide # the correct argument everytime. # Usage: ImageHeight = HeightOf(CANVASREF) # sub HeightOf { my $REF = $_[0]; return vec($$REF, 3, 32); } # Returns the image depth (bytes per pixel) of a canvas. # No error checking is done, so make sure to provide # the correct argument everytime! # Usage: BytesPerPixel = DepthOf(CANVASREF) # sub DepthOf { my $REF = $_[0]; defined $$REF && length($$REF) > 7 or return 0; no warnings; my $D = substr($$REF, 6, 2) >> 3; use warnings; return $D; } # This function returns a canvas reference if the # first argument holds a valid canvas reference. # Otherwise this function returns zero! # # Usage: CANVASREF = GetCanvasRef(CANVAS OR CANVASREF) # sub GetCanvasRef { defined $_[0] && ref($_[0]) eq 'SCALAR' or return 0; my $REF = $_[0]; defined $$REF && length($$REF) > 15 or return 0; substr($$REF, 0, 6) eq 'CANVAS' or return 0; return (index('08162432', substr($$REF, 6, 2)) & 1) ? 0 : $REF; } # This function makes sure that the canvas string is # not too short. If parts of the image are missing, # they are filled in with black pixels. # # Usage: FixCanvas(CANVASREF, [Width, [Height, [Depth]]]) # sub FixCanvas { my $REF = GetCanvasRef($_[0]) or return 0; my $W = defined $_[1] ? $_[1] : WidthOf($REF); my $H = defined $_[2] ? $_[2] : HeightOf($REF); my $D = GetBPP($_[3]) || DepthOf($REF); FixCanvasHeader($REF, $W, $H, $D); my $SIZE = $W * $H * $D + 16; if (length($$REF) < $SIZE) { vec($$REF, $SIZE - 1, 8) = 0; } if (length($$REF) > $SIZE) { $$REF = substr($$REF, 0, $SIZE); } return 1; } # # Canvas | Graphics | v2022.12.13 # Returns a reference to a 0 x 0 blank canvas. # Usage: CANVASREF = BlankCanvas([DEPTH]) sub BlankCanvas { return NewCanvas(0, 0, $_[0]); } # This function forces the INPUT_NUMBER to become # and integer between MIN and MAX values. # If INPUT_NUMBER is smaller than MIN, then return MIN. # If INPUT_NUMBER is greater than MAX, then return MAX. # # Usage: INTEGER = IntRange(INPUT_NUMBER, MIN, MAX) # sub IntRange { no warnings; my $MIN = defined $_[1] ? int($_[1]) : 0; my $NUM = defined $_[0] ? int($_[0]) : $MIN; my $MAX = defined $_[2] ? int($_[2]) : 4294967295; use warnings; $NUM > $MIN or return $MIN; $NUM < $MAX or return $MAX; return int($NUM); } # # Math | v2022.10.21 # This function counts how many 1s occur in a # 32-bit integer when converted to binary format. # (This function actually doesn't do any counting; # it uses a lookup table to get the answer.) # # Usage: INTEGER = CountBits32(INTEGER) # sub CountBits32 { my $V = $_[0] & 0xffffffff; my $T = "\x10!!2!22C!22C2CCT!22C2CCT2CCTCTTe!22C2CCT2CCTCTTe2CCTCTTe +CTTeTeev!22C2CCT2CCTCTTe2CCTCTTeCTTeTeev2CCTCTTeCTTeTeevCTTeTeevTeeve +vv\x87"; # According to the order of precedence, the shift >> operator # is evaluated first, then the bitwise & operator is second, # which is quite convenient for us here. return vec($T, $V & 255, 4) + vec($T, $V >> 8 & 255, 4) + vec($T, $V >> 16 & 255, 4) + vec($T, $V >> 24 & 255, 4); } # # Math | v2022.8.28 # This function raises X to the Nth power. # Usage: INTEGER = POWER(X, N) # sub POWER { my $X = defined $_[0] ? $_[0] : 0; my $N = defined $_[1] ? $_[1] : 0; $N > 0 or return 1; my $PWR = 1; while ($N-- > 0) { $PWR *= $X; } return $PWR; } # # Math | v2022.10.23 # This function forces a number to become a 32-bit # integer and returns the negated value of that integer. # # Usage: INTEGER = NEG32(NUMBER) # sub NEG32 { return defined $_[0] && $_[0] ? ~$_[0] + 1 & 0xffffffff : +0; } # # Math | v2022.10.23 # Like the name suggests, this function takes any # number as input and returns an integer that # is rounded up to the next higher value. # # Usage: INTEGER = CEIL(NUMBER) # sub CEIL { return int($_[0]) + ($_[0] - int($_[0]) > 0); } # # Debugging | v2022.12.11 # This function is called whenever information or # error messages are printed by any of the graphics # routines in this library. This function can be # modified to print everything to stderr or to save # the messages to a file or suppress everything. # Usage: GPRINT(MSG) # sub GPRINT { print @_; } # # BMP | Graphics | v2022.11.15 # Use this function to read and decode any kind of # BMP file. Returns a canvas object. Returns a # 0 x 0 blank canvas if something goes wrong. # # The first argument is the BMP file name. # The second argument tells this function how many # bytes to use to store each pixel. There are only # three valid values: 1, 3, 4. # # A BMP file can have a number of different formats # and can use 1 bit, 4 bits, 8 bits, 16 bits, 24 bits, # or 32 bits to represent one pixel. However, at # the time when the image is decoded and stored in # memory, the image can be upscaled or downscaled. # # This function supports all types of BMP formats. # It can read old OS/2 BMP images, RLE compressed # BMP images, standard BMP images (no compression), # and custom format BMP images with or without # palette and transparency. # # Usage: CANVASREF = ReadBMP(FILENAME, [DEPTH]) # sub ReadBMP { my $D = GetBPP($_[1], 3); my $BMPINFO = ReadBMPHeader($_[0]); my $FMT = vec($BMPINFO, 0, 16); if ($FMT == 0x100) { return ReadStandardBMP($BMPINFO, $D); } if ($FMT == 0x200) { return ReadCustomBMP($BMPINFO, $D); } if ($FMT == 0x300) { return ExpandRLE($BMPINFO, $D); } return BlankCanvas($D); } # # BMP | Graphics | v2022.11.17 # This function reads the color palette from a # BMP file's header and returns it as a 1024-byte # string in which each color takes up 4 bytes, # starting with alpha (transparency) value, which # is followed by the red, green, and blue values. # Missing colors are filled with zero bytes. # # The 1st argument (HEADER) must be a string that # contains the first 1200 bytes of a BMP file. # The 2nd argument (PALPTR) is a pointer to where # the palette begins within the header. # The 3rd argument (PALWIDTH) tells the function # whether the palette is 3 or 4 bytes per color. # The 4th argument (CC) is the number of colors # in the palette. # # Usage: PALETTE = ReadBMPPalette(HEADER, PALPTR, PALWIDTH, CC) # sub ReadBMPPalette { @_ == 4 or return ''; foreach (@_) { defined $_ or return ''; } my $PALPTR = $_[1]; my $PALWIDTH = $_[2]; my $CC = $_[3]; $PALPTR > 12 or return ''; # Initialize palette. my $PALETTE = ''; vec($PALETTE, 1023, 8) = 0; # Fill with zero bytes. my ($R, $G, $B, $A) = (0) x 4; # In the BMP header, each color is stored usually in 4 bytes, # sometimes 3 bytes. And they are stored first starting with # the blue value, then green, red, and finally the alpha. for (my $i = 0; $i < $CC; $i++) { $B = vec($_[0], $PALPTR++, 8); $G = vec($_[0], $PALPTR++, 8); $R = vec($_[0], $PALPTR++, 8); $A = vec($_[0], $PALPTR++, 8) if ($PALWIDTH == 4); vec($PALETTE, $i, 32) = $A << 24 | $R << 16 | $G << 8 | $B; } return $PALETTE; } # # BMP | Graphics | v2022.11.21 # This function returns all the values that are # stored in the BMPINFO string. # Usage: ARRAY = UnpackBMPINFO(BMPINFO) # sub UnpackBMPINFO { defined $_[0] && length($_[0]) > 1150 or return (); my @L = unpack('C5V24d3', $_[0]); push(@L, substr($_[0], 125, 1024)); push(@L, substr($_[0], 1151)); return @L; } # # BMP | Graphics | v2022.11.21 # This function reads a BMP file's header and # returns a bunch of values encapsulated in a # string using the pack() function. # # Usage: BMPINFO = ReadBMPHeader(FILENAME) # sub ReadBMPHeader { my $F = FilterFileName($_[0]); my ($BMPINFO, $HEADER, $FMT, $E) = ('', '', 0, 0); # The following foreach() loop allows us to exit the function # conveniently using a common exit route. Everything inside the # loop will run only once. If there is an error, we skip to the # end quickly using the "last" statement. If there are no # errors, we go through all the steps and exit at the bottom # at the same place. $E will hold the error code. # If no errors occurred, then $E will be zero. foreach (0) { # Read the first 1200 bytes from the file. ($E = ReadFile($_[0], $HEADER, 0, 1200)) and last; vec($HEADER, 1200, 8) = 0; # Expand header if it was shorter. # Unpack header values. my ($SIG, $FILESIZE, $RESERVED, $DATAPTR, $BMPVER) = unpack('vV4', $HEADER); my ($W, $H, $PLANES, $BPP) = unpack($BMPVER < 16 ? 'v4' : 'VVvv', substr($HEADER, 18, 12)); my ($COMPR, $DATASIZE, $XRES, $YRES, $COLORS, $IC) = $BMPVER > 16 ? unpack('V6', substr($HEADER, 30, 24)) : (0) x 6; my $BGRS = ($BPP <= 8 && substr($HEADER, 54, 4) eq 'BGRs') & 1; # Check file signature. if ($SIG != 0x4D42) { $E = 4; last; } # Not a BMP file # Figure out what kind of encoding is used. if ($COMPR == 0) { $FMT = 1; } # Standard (raw) elsif ($BMPVER >= 56 && $DATAPTR >= 70 && $BPP >= 16 && $COMPR == 3) { $FMT = 2; } # Custom format elsif (($BPP == 4 || $BPP == 8) && ($COMPR == 1 || $COMPR == 2)) { $FMT = 3; } # RLE Compressed else { $E = 5; print last; } # Corrupt file # Read bit masks for custom format. my ($RMASK, $GMASK, $BMASK, $AMASK) = ($FMT == 2) ? unpack('V4', substr($HEADER, 54, 16)) : (0) x 4; # Calculate image height. my $VFLIP = 1; # VFLIP=1 means the image is stored upside down if ($H & 0x80000000) { $VFLIP = 0; $H = NEG32($H); } if ($W == 0 || $H == 0) { $E = 6; last; } # Copy palette from BMP header. my $MAXCOLORS = 16777216; if ($FMT == 2) { $MAXCOLORS = POWER(2, CountBits32($RMASK | $GMASK | $BMASK)); } elsif ($BPP < 24) { $MAXCOLORS = 1 << $BPP; } my $PALPTR = $BMPVER + 14; my $PALWIDTH = $BMPVER < 16 ? 3 : 4; # $CC is the COLOR COUNT. my $CC = $COLORS && $COLORS < $MAXCOLORS ? $COLORS : $MAXCOLORS; if ($BPP > 8) { $CC = $PALWIDTH = $PALPTR = 0; } my $PALETTE = ReadBMPPalette($HEADER, $PALPTR, $PALWIDTH, $CC); # Perform some calculations... my $ROWLEN = CEIL(($W * $BPP) / 8); # Bytes per row my $PADDING = (4 - ($ROWLEN & 3)) & 3; # Padding bytes per row my ($DIR, $START, $STOP) = (1, 0, $H); if ($VFLIP) { ($DIR, $START, $STOP) = (-1, $H - 1, -1); } $ROWLEN += $PADDING; # Everything seems to be OK. $BMPINFO = pack('C5V24d3', $FMT, $E, $BGRS, $PALWIDTH, $VFLIP, $W, $H, $COMPR, $BMPVER, $COLORS, $XRES, $YRES, $MAXCOLORS, $FILESIZE, $DATASIZE, $DATAPTR, $RESERVED, $IC, $CC, $SIG, $RMASK, $GMASK, $BMASK, $AMASK, $BPP, $PLANES, $ROWLEN, $PALPTR, $PADDING, $START, $STOP, $DIR) . $PALETTE; } vec($BMPINFO, 1, 8) = $E; # Save error code vec($BMPINFO, 1150, 8) = 0; # Expand BMPINFO undef $HEADER; return $BMPINFO . $F; } # # BMP | Graphics | v2022.11.16 # This function reads a BMP file that uses the simplest # form of encoding. Returns a reference to a canvas object. # # Usage: CANVASREF = ReadStandardBMP(BMPINFO, [DEPTH]) # sub ReadStandardBMP { my ($FMT, $E, $BGRS, $PALWIDTH, $VFLIP, $W, $H, $COMPR, $BMPVER, $COLORS, $XRES, $YRES, $MAXCOLORS, $FILESIZE, $DATASIZE, $DATAPTR, $RESERVED, $IC, $CC, $SIG, $RMASK, $GMASK, $BMASK, $AMASK, $BPP, $PLANES, $ROWLEN, $PALPTR, $PADDING, $START, $STOP, $DIR, $PALETTE, $F) = UnpackBMPINFO($_[0]); my $D = GetBPP(defined $_[1] ? $_[1] : $BPP, 3); print "\nReading BMP file: $F (", Commify(-s $F), ' bytes)', "\nDecoding $W x $H x $BPP uncompressed BMP image..."; my ($BYTE, $COLOR, $PX, $A, $R, $G, $B) = (0) x 7; my $SHIFT = 7; # The $SHIFT variable is only used when reading # monochrome bitmaps where each bit represents one pixel, so we # have to shift the bits left to extract them. The first pixel is # always stored in the highest bit, so we start with $SHIFT = 7. # Create canvas right here. my $CANVAS = CreateCanvasHeader($W, $H, $D); vec($CANVAS, $W * $H * $D + 15, 8) = 0; # Fill canvas. my $P = 16; # Canvas byte pointer to first pixel # Open file for read only. local *FILE; sysopen(FILE, $F, 0) or return \$CANVAS; binmode FILE; for (my $Y = $START; $Y != $STOP; $Y += $DIR) { # Move the file pointer to the beginning of row $Y #print "\nW=$W H=$H START=$START STOP=$STOP VFLIP=$VFLIP Y=$Y ROWL +EN=$ROWLEN"; seek(FILE, $Y * $ROWLEN + $DATAPTR, 0); for (my $X = 0; $X < $W; $X++) { if ($BPP <= 8) { # Read 8-bit pixel: if ($BPP == 8) { $COLOR = ord(getc(FILE)); } # Read 4-bit pixel: elsif ($BPP == 4) { $COLOR = ($X & 1) ? $BYTE & 15 : ($BYTE = ord(getc(FILE))) >> 4; } # Read 1-bit pixel: elsif ($BPP == 1) { $COLOR = ($X & 7) ? ($BYTE >> --$SHIFT) & 1 : ($BYTE = ord(getc(FILE))) >> ($SHIFT = 7); } # Look up R G B values in palette if we have to upscale # the image from 8bpp to 24bpp or 32bpp. if ($D >= 3 && $BPP <= 8) { $COLOR <<= 2; $A = vec($PALETTE, $COLOR, 8); $R = vec($PALETTE, $COLOR+1, 8); $G = vec($PALETTE, $COLOR+2, 8); $B = vec($PALETTE, $COLOR+3, 8); } } elsif ($BPP >= 24) # Read 24-bit or 32-bit pixel { $B = ord(getc(FILE)); $G = ord(getc(FILE)); $R = ord(getc(FILE)); $A = ord(getc(FILE)) if ($BPP == 32); if ($D == 1) { $COLOR = Match_Palette_Color($PALETTE, $R, $G, $B); } } # Save pixel to canvas as 8-bit, 24-bit, or 32-bit: if ($D == 1) { vec($CANVAS, $P++, 8) = $COLOR; } else { if ($D > 3) { vec($CANVAS, $P++, 8) = $A; } substr($CANVAS, $P, 3) = pack('CCC', $R, $G, $B); $P += 3; } $PX++ < 10000 or $PX = print '.'; } } close FILE; print "\nDONE.\n"; return \$CANVAS; } # # Canvas | Graphics | v2022.11.21 # This function creates a new canvas object in # memory and returns its reference. # # Usage: CANVASREF = NewCanvas(Width, Height, Depth, [BgColor]) # sub NewCanvas { my $W = IntRange($_[0], 0, 4294967295); # Width my $H = IntRange($_[1], 0, 4294967295); # Height my $D = GetBPP($_[2], 3); # Convert depth to bytes per pixel my $C = Int32bit($_[3]); # Background color my $CANVAS = CreateCanvasHeader($W, $H, $D); my $RES = $W * $H; $RES or return \$CANVAS; # Use zero (black) as the background? if ($C == 0) { vec($CANVAS, ($RES * $D + 15), 8) = 0; return \$CANVAS; } # Convert color to a string. $C = toColorStr($C, $D); $CANVAS .= $C x $RES; # Fill canvas return \$CANVAS; } # # Graphics | v2022.11.20 # This function creates a lookup table for color # enhancement. The function expects one integer # that tells it how many bits are used to # represent a particular RGB channel. # # WHAT DOES THIS FUNCTION REALLY DO AND WHY ? # # We take each color component and stretch its normal # range of values so its new range will be 0 to 255. # For example, if a red value is stored in 4 bits, # then we have a possible range of 0000 to 1111 in # binary. That's 0 to 15. Now, we could put 4 # zero bits behind this, but our range would still be # from 00000000 to 11110000 (0-240). This means that # the brightest white in the picture would still look # a bit greyish. This is what happens when you chop # off the least significant bits of the R G B values. # So, to fix this, we multiply each R G B color component # by a certain amount, so when they are at maximum, the # intensity will be 255 instead of 240. But instead of # multiplying, we use a lookup table, which is a whole # lot faster. This function builds the lookup table. # # EXAMPLE. How to use the lookup table: # # $RX = CountBits32($RMASK); # 0000f000 => 4 # $GX = CountBits32($GMASK); # 00000f00 => 4 # $BX = CountBits32($BMASK); # 000000f0 => 4 # $RLT = BuildColorStretchTable($RX); # $GLT = BuildColorStretchTable($GX); # $BLT = BuildColorStretchTable($BX); # ... # $RED = vec($RLT, $RED, 8); # Input: 240 Output: 255 # $GRN = vec($GLT, $GRN, 8); # Input: 16 Output: 17 # $BLU = vec($BLT, $BLU, 8); # Input: 0 Output: 0 # # Usage: STRING = BuildColorStretchTable(BITCOUNT) # sub BuildColorStretchTable { my $N = $_[0]; $N > 0 or return ''; # What's the biggest number we can arrange using $N number of bits? my $MAX = (1 << $N) - 1; my $LUT = ''; vec($LUT, $MAX, 8) = 0; # Reserve memory for the lookup table. # If colors are represented with 8 bits, then we don't # need to stretch anything at all. In other words, # the output is going to be the same as the input. # So, here we build a lookup table that does that: if ($N >= 8) { for (my $i = 1; $i < 256; $i++) { vec($LUT, $i, 8) = $i; } return $LUT; } # Calculate multiplier. my $MULTIPLIER = 255 / $MAX; # Here, we will build the lookup table: for (my $i = 1; $i <= $MAX; $i++) { vec($LUT, $i, 8) = ($i * $MULTIPLIER) & 255; } return $LUT; } # # BMP | Graphics | v2022.11.19 # This function reads a custom format BMP file. # Returns a reference to a canvas object. # # Usage: CANVASREF = ReadCustomBMP(BMPINFO, DEPTH) # # BMPINFO: The first argument must be a special string # that is generated by the ReadBMPHeader() function. # # DEPTH: The second argument is the requested image depth # for the canvas. This value may be provided in bytes per pixel # or bits per pixel. Valid values are: 1, 3, 4, 8, 24, 32. # # CANVASREF: The return value of this function is a reference # that points to a string which contains the image data. # The first 8 bytes of this string will contain the word # "CANVAS24" or "CANVAS32" depending on the encoding, followed # by the width and height of the image which are encoded as # two 32-bit unsigned integers stored in big-endian format. # After this 16-byte header, the pixels are stored in raw # format starting with the first pixel in the upper left corner. # When "CANVAS24" is used, the pixels are in RGB order. # When "CANVAS32" is used, the pixels are in ARGB order. # The canvas contains no padding at all, just raw data. # # WHAT IS CUSTOM FORMAT ? # # Custom format means that the BMP header includes # four 32-bit integers which are used as bit masks # that tell us where the bits are stored for red, # green, blue and alpha values. Here is an example: # AMASK=0000000f This tells us that the alpha value # RMASK=000000f0 is stored in the lowest 4 bits, # GMASK=00000f00 followed by red, which is stored # BMASK=0000f000 in the next 4 bits, then 4-bits # for green, and 4 bits for blue. We would represent # this encoding as A4 R4 G4 B4. As you can see, this # adds up to 16 bits. So, that's 16 bits per pixel. # # You will find this representation in Adobe PhotoShop. # When you save a picture in BMP format, it gives you a # number of options such as A1 R5 G5 B5, A8 R8 G8 B8, # R5 G6 B5, and others. There are many possibilities. # # Unfortunately, most of these special formats result in # a loss of quality. For example, if the picture includes # a purple color such as R=204 G=83 B=255 and we wanted to # store it in 16 bits in the format specified above, we # would start out like this: R=11001100 G=01010011 B=11111111 # Then we will keep only the high 4 bits R=1100 G=0101 B=1111 # and then join them together to form one 16-bit number: # 1111 + 0101 + 1100 + 0000 => 1111010111000000 # So, that's how we store one pixel in custom format. # # For decoding, we do the same steps in reverse. # We use the bit masks to extract the values from one # 16-bit pixel: 1111010111000000 # RED MASK : 0000000011110000 # RED VALUE : --------1100---- # RED VALUE : 1100 # RED VALUE : 11000000 # # 16-bit pixel: 1111010111000000 # GREEN MASK : 0000111100000000 # GREEN VALUE : ----0101-------- # GREEN VALUE : 0101 # GREEN VALUE : 01010000 # # So, we will have R=1100 G=0101 B=1111 which becomes # R=11000000 G=01010000 B=11110000 (R=192 G=80 B=240). # So, the original color was R=204 G=83 B=255, and you # can see that we ended up with a slightly different # color. It's still a purple, but it's a little bit off. # To try to correct this problem, we use a color stretch # lookup table. See BuildColorStretchTable() for more info. # # When using custom format, the Compression value must # be set to 3, and the Bits Per Pixel value can be 16, # 24 or 32. The header must use BMP version 56 or above. # # Usage: CANVASREF = ReadCustomBMP(BMPINFO, DEPTH) # sub ReadCustomBMP { my ($FMT, $E, $BGRS, $PALWIDTH, $VFLIP, $W, $H, $COMPR, $BMPVER, $COLORS, $XRES, $YRES, $MAXCOLORS, $FILESIZE, $DATASIZE, $DATAPTR, $RESERVED, $IC, $CC, $SIG, $RMASK, $GMASK, $BMASK, $AMASK, $BPP, $PLANES, $ROWLEN, $PALPTR, $PADDING, $START, $STOP, $DIR, $PALETTE, $F) = UnpackBMPINFO($_[0]); my $D = GetBPP(defined $_[1] ? $_[1] : $BPP, 3); print "\nReading BMP file: $F (", Commify(-s $F), ' bytes)', "\nDecoding $W x $H custom format BMP image ", "($BPP bpp => ", ($D << 3), ' bpp) '; my ($PX, $PIXEL, $A, $R, $G, $B) = (0) x 7; # Okay. This is just preparation work. # Here we figure out how many bits are set in each mask. my $RX = CountBits32($RMASK); my $GX = CountBits32($GMASK); my $BX = CountBits32($BMASK); my $AX = CountBits32($AMASK); # Here we figure out how much we have to shift a pixel's value # to the right in order to extract the individual R G B A values. my $RSHIFT = ZeroCountR32($RMASK) + ($RX > 8 ? $RX - 8 : 0); my $GSHIFT = ZeroCountR32($GMASK) + ($GX > 8 ? $GX - 8 : 0); my $BSHIFT = ZeroCountR32($BMASK) + ($BX > 8 ? $BX - 8 : 0); my $ASHIFT = ZeroCountR32($AMASK) + ($AX > 8 ? $AX - 8 : 0); # Here we build two separate lookup tables for # enhancing the R G B values and alpha: my $RLT = BuildColorStretchTable($RX); my $GLT = BuildColorStretchTable($GX); my $BLT = BuildColorStretchTable($BX); my $ALT = BuildColorStretchTable($AX); # Create canvas right here. my $CANVAS = CreateCanvasHeader($W, $H, $D); vec($CANVAS, $W * $H * $D + 15, 8) = 0; # Fill canvas. my $P = 16; # Canvas byte pointer to first pixel # Create 256 color palette if we have to downscale # the image to 8-bit from 16-bit, 24-bit, or 32-bit. if ($D == 1) { $PALWIDTH = 4; } # Open file for read only. local *FILE; sysopen(FILE, $F, 0) or return \$CANVAS; binmode FILE; for (my $Y = $START; $Y != $STOP; $Y += $DIR) { # Move the file pointer to the beginning of row $Y seek(FILE, $Y * $ROWLEN + $DATAPTR, 0); for (my $X = 0; $X < $W; $X++) { # Read one pixel: $PIXEL = ord(getc(FILE)); $PIXEL |= ord(getc(FILE)) << 8; $BPP <= 16 or $PIXEL |= ord(getc(FILE)) << 16; $BPP <= 24 or $PIXEL |= ord(getc(FILE)) << 24; # Extract R G B A values and do some color enhancement: $R = vec($RLT, ($RMASK & $PIXEL) >> $RSHIFT, 8); $G = vec($GLT, ($GMASK & $PIXEL) >> $GSHIFT, 8); $B = vec($BLT, ($BMASK & $PIXEL) >> $BSHIFT, 8); $A = vec($ALT, ($AMASK & $PIXEL) >> $ASHIFT, 8); # Write pixel to canvas: # If we have to save a 16-bit, 24-bit or 32-bit # pixel as 8-bit, then we convert it first. if ($D == 1) { vec($CANVAS, $P++, 8) = Match_Palette_Color($PALETTE, $R, $G, $B); } else # 24-bit or 32-bit: { $D < 4 or vec($CANVAS, $P++, 8) = $A; if ($D >= 3) { substr($CANVAS, $P, 3) = pack('CCC', $R, $G, $B); $P += 3; } } $PX++ < 10000 or $PX = print '.'; } } close FILE; print "\nDONE.\n"; return \$CANVAS; } # # Palette | v2022.9.27 # This function returns a color index that points # to a palette color that is the closest match to # the original R G B values provided. This function # is used when downscaling a truecolor bitmap from # 16 million colors to 16 colors or 256 colors, and for # each RGB pixel, we must find a color in the palette # that most closely resembles the original color. # # NOTE: No error checking is done, so make sure you # pass the right arguments every time! # # Usage: COLOR_INDEX = Match_Palette_Color(PALETTE, R, G, B) # sub Match_Palette_Color { my ($i, $C, $PREV, $DIFF, $PALPTR) = (0) x 5; my $L = length($_[0]); my $LEAST_DIFF = 777; for (; $PALPTR < $L; $PALPTR += 4, $i++) { $DIFF = abs(vec($_[0], $PALPTR + 1, 8) - $_[1]) + abs(vec($_[0], $PALPTR + 2, 8) - $_[2]) + abs(vec($_[0], $PALPTR + 3, 8) - $_[3]); if ($DIFF == 0) { return $i; } if ($DIFF < $LEAST_DIFF) { $LEAST_DIFF = $DIFF; $PREV = $C; $C = $i; } } return $C; } # # Graphics | v2022.11.14 # This function flips an image vertically. # Supports 8-bit, 24-bit and 32-bit images. # # Usage: STATUS = FlipVertical(CANVASREF) # sub FlipVertical { my ($REF, $W, $H, $D, $START) = UseCanvas($_[0]) or return 0; # If the entire image is just one line, there is nothing to do. $W > 0 && $H > 1 or return 1; my $COUNT = $H >> 1; my $ROWLEN = $W * $D; my $FROM = $START; my $TO = $ROWLEN * $H + $START; while ($COUNT--) { $TO -= $ROWLEN; my $LINE = substr($$REF, $FROM, $ROWLEN); # Copy entire line substr($$REF, $FROM, $ROWLEN) = substr($$REF, $TO, $ROWLEN); substr($$REF, $TO, $ROWLEN) = $LINE; $FROM += $ROWLEN; } return 1; } # # BMP | Graphics | v2022.11.17 # This function expands RLE4 and RLE8 compressed BMP # files and returns a reference to a canvas object. # If an error occurs, then returns a reference # to a blank (0x0) canvas. # # BMPINFO: The first argument must be a special string # that is generated by the ReadBMPHeader() function. # # DEPTH: The second argument is the requested image depth # for the canvas. This can be provided in bits per pixel or # bytes per pixel. Valid values are: 1, 3, 4, 8, 24, 32. # By default, all RLE compressed BMP images are 32-bit which # includes transparency, but you may request 8-bit or 24-bit # in which case the image will be downscaled automatically. # # Usage: CANVASREF = ExpandRLE(BMPINFO, DEPTH) # sub ExpandRLE { my ($FMT, $E, $BGRS, $PALWIDTH, $VFLIP, $W, $H, $COMPR, $BMPVER, $COLORS, $XRES, $YRES, $MAXCOLORS, $FILESIZE, $DATASIZE, $DATAPTR, $RESERVED, $IC, $CC, $SIG, $RMASK, $GMASK, $BMASK, $AMASK, $BPP, $PLANES, $ROWLEN, $PALPTR, $PADDING, $START, $STOP, $DIR, $PALETTE, $F) = UnpackBMPINFO($_[0]); my $D = GetBPP(defined $_[1] ? $_[1] : $BPP, 3); my $DEBUG = 0; print "\nExpanding $W x $H RLE compressed BMP image ", "($BPP bpp => ", ($D << 3), ' bpp) '; # Create canvas right here. my $CANVAS = CreateCanvasHeader($W, $H, $D); vec($CANVAS, $W * $H * $D + 15, 8) = 0; # Fill canvas. if ($D == 1) { $CANVAS .= $PALETTE; } my $P = 16; # Canvas byte pointer to first pixel my $PX = 0; $ROWLEN = $W * $D; # Output bytes per row my ($X, $Y, $MODE, $COUNT, $REPEAT, $SKIP, $PIX1, $PIX2) = (0) x 8; # Initialize some variables. sysopen(FILE, $F, 0) or return \$CANVAS; binmode FILE; sysseek(FILE, $DATAPTR, 0); my $RUN = 1; while ($RUN) { $PX++ < 1000 or $PX = print '.'; # Read file one byte at a time. Convert the byte to ASCII code. # After we reach the end of file, we read zeros. my $c = getc(FILE); $RUN = defined $c; $c = ($RUN) ? ord($c) : 0; if ($MODE < 0) { $MODE++; next; } # Skip padding character. if ($MODE == 0) # First byte { # If the first byte is zero: the next byte is going to be # a control character, which tells us what to do next... # If the first byte is non-zero: then we're looking at # a compressed chunk. $MODE = $c + 1; # Remember this and read next byte. next; } if ($MODE == 1) # 2nd byte: Control character! { if ($c == 0) # END OF LINE. { $X = 0; $Y = IntRange($Y + 1, 0, $H); $P = $Y * $W * $D + 16; $MODE = 0; next; } elsif ($c == 1) # END OF BITMAP. { last; } elsif ($c == 2) # MOVE PEN. { $MODE = 300; next; } else # Uncompressed block comes next { $COUNT = $c; $MODE = 500; # Uncompressed blocks in RLE mode must end on a word boundary, so # sometimes the block will be followed by a zero byte. We control # this by setting the $SKIP value, which later sets $MODE to -1, # which then causes the one byte to be read and discarded. # # Adobe PhotoShop and others include a padding byte when required, # but XnView leaves the padding off in RLE4 mode. This means # the resulting file will be smaller, but this is non-standard # practice which prevents certain programs from decoding the # file correctly. For example, Windows Paint will not open # 16-color BMP files compressed with XnView. This discrepancy is # hard to detect, but apparently, when $DATASIZE is zero, then # no padding is added. So, in the next few lines we try to # figure out when we need to skip a byte and when we don't: if ($BPP == 8) { $SKIP = $COUNT & 1; } else # RLE8 padding { $SKIP = ($DATASIZE) ? ($COUNT & 2) : 0; } # RLE4 padding next; } } elsif ($MODE <= 256) # 2nd byte: Compressed data comes next { $COUNT = $MODE - 1; $MODE = 600; } elsif ($MODE == 300) # Move pen. STEP 1. { $X += ($c < 128) ? $c : $c - 256; # Update X coordinate $X = IntRange($X, 0, $W); $MODE = 330; # Goto step 2 now. $DEBUG and print "\n\tMOVE PEN: X = $X"; next; } elsif ($MODE == 330) # Move pen. STEP 2. { $Y += ($c < 128) ? $c : $c - 256; # Update Y coordinate $Y = IntRange($Y, 0, $H); $P = ($Y * $ROWLEN) + ($X * $D) + 16; # Move pointer $MODE = 0; # We're done. $DEBUG and print "\n\tMOVE PEN: Y = $Y"; next; } if ($MODE > 400) # Write pixel(s) { if ($MODE == 500) # Prepare for writing uncompressed bytes. { $REPEAT = ($COUNT == 1 || $BPP == 8) ? 1 : 2; $COUNT -= ($BPP == 8) ? 1 : 2; if ($COUNT <= 0) { $MODE = ($SKIP) ? -1 : 0; $SKIP = 0; } } elsif ($MODE == 600) # Prepare for repeating pixels { $REPEAT = $COUNT; $MODE = 0; } # In RLE8 mode, each byte ($c) holds the color of one pixel. # In RLE4 mode, each byte ($c) holds two pixels. First pixel # is in the upper 4 bits; the second is in the lower 4 bits. # We break this down into $PIX1 and $PIX2. Then in the # for loop below, we alternate between PIX1 and PIX2 as we # write the pixels one by one. if ($BPP == 4) { $PIX1 = ($c >> 4) & 15; $PIX2 = $c & 15; } for (my $i = 0; $i < $REPEAT; $i++) { if ($BPP == 4) { $c = ($i & 1) ? $PIX2 : $PIX1; } if ($Y < 0 || $Y >= $H) { last; } if ($X++ < 0 || $X > $W) { next; } if ($D == 1) { # Write pixel to 8bpp canvas: vec($CANVAS, $P++, 8) = $c; } else { # Write pixel to 24bpp canvas: my $A = vec($PALETTE, $c, 32); # Lookup RGB values my $R = ($A >> 16) & 255; my $G = ($A >> 8) & 255; my $B = $A & 255; $A = ($A >> 24) & 255; if ($D == 4) { vec($CANVAS, $P++, 8) = $A; } # 32bpp vec($CANVAS, $P++, 8) = $R; vec($CANVAS, $P++, 8) = $G; vec($CANVAS, $P++, 8) = $B; } #### End of write pixel } ###### End of repeat pixel } ######## End of $MODE select } ########## End of main loop close FILE; if ($VFLIP) { FlipVertical(\$CANVAS); } print "\nDONE.\n"; return \$CANVAS; } # # v2022.9.5 # This function removes illegal characters from # a file name such as: $ % ? * < > | " \t \r \n \0 # and any character whose ASCII value is 0-31. # # Usage: FILENAME = FilterFileName(FILENAME) # sub FilterFileName { my $F = defined $_[0] ? $_[0] : ''; $F =~ tr`<>*%$?\x00-\x1F\"\|``d; return $F; } # # File | v2022.11.17 # Reads an entire binary file or part of a file. # This function uses sysopen(), sysseek(), and # sysread() functions. Unlike many other perl subs, # this function returns 0 on success or an error code: # 1=File Not Found, 2=Not Plain File, 3=Cannot Open For Reading # If an error occurs then the buffer will hold an empty string. # # The first argument is the file name. # The second argument is a string buffer. (The buffer doesn't # have to be initialized. It may contain an undefined value.) # An optional 3rd argument (integer) will move # the file pointer before reading, and an optional # 4th argument (integer) can limit the number of # bytes to read. These numbers cannot be negative. # If the number of bytes to read is set to zero, # then it will read the entire file. (default) # # Usage: STATUS = ReadFile(FILENAME, BUFFER, [START, [LENGTH]]) # sub ReadFile { my $F = defined $_[0] ? $_[0] : ''; # Get file name. $F =~ tr/\x00-\x1F\"\|*%$?<>//d; # Remove illegal characters. my $FP = defined $_[2] ? $_[2] : 0; # File Pointer my $N = defined $_[3] ? $_[3] : 0; # Number of bytes to read $_[1] = ''; # Initialize read buffer. -e $F or return 1; # File exists? -f $F or return 2; # Is it a plain file? my $SIZE = -s $F; # Get file size. # Make sure all parameters are valid. if ($N < 0 || $FP < 0 || $FP >= $SIZE) { return 0; } $SIZE -= $FP; if ($N == 0 || $N > $SIZE) { $N = $SIZE; } local *FILE; sysopen(FILE, $F, 0) or return 3; # Open file for read only. $FP && sysseek(FILE, $FP, 0); # Move file pointer sysread(FILE, $_[1], $N); # Read N bytes close FILE; return 0; } # # File | v2022.11.8 # Creates and overwrites a file in binary mode. # If the file has already existed, it erases the # old content and replaces it with the new content. # Returns 1 on success or 0 if something went wrong. # # Usage: STATUS = CreateFile(FILENAME, CONTENT) # sub CreateFile { my $F = defined $_[0] ? $_[0] : ''; $F =~ tr/\x00-\x1F\"\|*%$?<>//d; # Remove illegal characters. my $L = defined $_[1] ? length($_[1]) : 0; local *FILE; open(FILE, ">$F") or return 0; binmode FILE; $L and print FILE $_[1]; close FILE; -e $F or return 0; # File exists? -f $F or return 0; # It's a plain file? $L -= -s($F); # Check file size. return !$L; } # # BMP | Graphics | v2022.11.26 # This function saves a canvas image as a BMP file. # You may use the 3rd and 4th arguments to specify # the exact output format. If you omit these, # we will automatically pick the best format. # # Usage: SaveBMP(CANVASREF, FILENAME, [FMT, [BMPVER]]) # # The 4th argument allows you to request that a particular # BMP header version be used, however this request may be # overridden by the 3rd argument. Also, if you specify an # invalid value, it will be replaced with the correct one. # Also note that BMP v40 is the standard format, and using # anything other than v40 means that some photo editors # and viewers will not be able to decode your BMP file. # # Recommended: Choose a format that has a * in front of it. # This means that all programs will be able to open your BMP file. # # FMT v12 v40 v56 BITS FORMAT COLORS # ------------------------------------------------------------- # 0 .......... *X X 32 A8 R8 G8 B8 16777216 truecolor # 1 ..... *X *X X 24 R8 G8 B8 16777216 truecolor # 2 ................ X 32 X8 R8 G8 B8 16777216 truecolor # 3 ................ X 16 R5 G6 B5 65536 truecolor # 4 ................ X 16 A1 R5 G5 B5 32768 truecolor # 5 ................ X 16 X1 R5 G5 B5 32768 truecolor # 6 ................ X 16 A4 R4 G4 B4 4096 truecolor # 7 ................ X 16 X4 R4 G4 B4 4096 truecolor # 8 ................ X 16 A8 C8 256 grayscale # ------------------------------------------------------------- # # Usage: SaveBMP(CANVASREF, FILENAME, [FMT, [BMPVER]]) # sub SaveBMP { my ($REF, $W, $H, $D, $START) = UseCanvas($_[0]) or return 0; my @FORMAT_DETAILS = ( ################## # BYTES PER PIXEL Minimum Header Version # Red Green Blue Alpha \ | Compression Value # Mask Mask Mask Mask \ | / Encoding MaxColors '00ff0000 0000ff00 000000ff ff000000 4 40 0 A8 R8 G8 B8 16777216', '00ff0000 0000ff00 000000ff 00000000 3 12 0 R8 G8 B8 16777216', '00ff0000 0000ff00 000000ff 00000000 4 56 3 X8 R8 G8 B8 16777216', '0000f800 000007e0 0000001f 00000000 2 56 3 R5 G6 B5 65536', '00007c00 000003e0 0000001f 00008000 2 56 3 A1 R5 G5 B5 32768', '00007c00 000003e0 0000001f 00000000 2 56 3 X1 R5 G5 B5 32768', '00000f00 000000f0 0000000f 0000f000 2 56 3 A4 R4 G4 B4 4096', '00000f00 000000f0 0000000f 00000000 2 56 3 X4 R4 G4 B4 4096', '000000ff 000000ff 000000ff 0000ff00 2 56 3 A8 L8 gray 256', '00000000 00000000 00000000 00000000 1 12 0 L8 gray 256', '00000000 00000000 00000000 00000000 1 40 1 L8 gray RLE8 256', '00000000 00000000 00000000 00000000 1 12 0 8-bit 256', '00000000 00000000 00000000 00000000 1 40 1 8-bit RLE8 256', '00000000 00000000 00000000 00000000 1 12 0 4-bit 16', '00000000 00000000 00000000 00000000 1 40 2 4-bit RLE4 16', '00000000 00000000 00000000 00000000 1 12 0 1-bit 2', '00000000 00000000 00000000 00000000 1 12 0 B/W dithered 2'); ################## # Get BMP Version my $BMPVER = defined $_[3] ? $_[3] : 40; if ($BMPVER <= 12) { $BMPVER = 12; } elsif ($BMPVER >= 56) { $BMPVER = 56; } else { $BMPVER = 40; } # Look up format details. my $FMT = defined $_[2] ? $_[2] : -1; if ($FMT < 0 || $FMT >= @FORMAT_DETAILS) # Autodetect format { $BMPVER = 40; $FMT = 1; if ($D == 1) { $FMT = 14; } elsif ($D == 4) { $FMT = 0; } } my $DETAILS = $FORMAT_DETAILS[$FMT]; my $COMPRESS = index($DETAILS, 'RLE') > 0; # Images whose width or height exceeds # 65535 pixels must use at least v40 header. # Update BMP Version. my $MINVER = substr($DETAILS, 38, 2); if ($BMPVER < $MINVER) { $BMPVER = $MINVER; } if ($BMPVER < 40 && ($W > 65535 || $H > 65535)) { $BMPVER = 40; } substr($DETAILS, 38, 2) = $BMPVER; vec($DETAILS, 66, 8) = 32; $DETAILS .= FilterFileName($_[1]); if ($FMT < 9) { return SaveTruecolorBMP($REF, $DETAILS); } if ($COMPRESS) { return CompressBMP($REF, $DETAILS); } return SaveBMPwithPalette($REF, $DETAILS); } # # BMP | Graphics | v2022.11.26 # This function unpacks the format details provided # by the SaveBMP() function for its private functions. # Usage: ARRAY = UnpackBMPFormatDetails(STRING) # sub UnpackBMPFormatDetails { my $F = defined $_[0] ? $_[0] : ''; if (length($F) < 55) { return (0) x 9; } my $BYTES_PER_PIXEL = substr($F, 36, 1); my $RMASK = hex(substr($F, 0, 8)); my $GMASK = hex(substr($F, 9, 8)); my $BMASK = hex(substr($F, 18, 8)); my $AMASK = hex(substr($F, 27, 8)); my $BMPVER = substr($F, 38, 2); my $COMPR = substr($F, 41, 1); my $ENCODING = Trim(substr($F, 43, 12)); my $MAXCOLORS = Trim(substr($F, 56, 8)); my $FILENAME = Trim(substr($F, 66)); return ($BYTES_PER_PIXEL, $RMASK, $GMASK, $BMASK, $AMASK, $BMPVER, $COMPR, $ENCODING, $MAXCOLORS, $FILENAME); } # # BMP | Graphics | v2022.11.26 # This is a private function that is called by the # SaveBMP() function to save a canvas image as a # BMP file in truecolor format. # # The second argument is special string that contains # all the necessary details about the encoding such as # bit masks, BPP, BMP version, compression, etc. # # Usage: SaveTruecolorBMP(CANVASREF, FORMAT_DETAILS) # sub SaveTruecolorBMP { my ($CANVAS, $W, $H, $DIN, $START) = UseCanvas($_[0]) or return 0; # $DIN contains the input image depth (bytes per pixel) # $DOUT contains the output image depth (bytes per pixel) my $PALETTE = ''; my ($DOUT, $RMASK, $GMASK, $BMASK, $AMASK, $BMPVER, $COMPR, $ENCODING, $MAXCOLORS, $F) = UnpackBMPFormatDetails($_[1]); my $RES = $W * $H; my $DATASIZE = $RES * $DOUT; # Adobe PhotoShop prefers when FILESIZE is divisible by 4. # (We'll add some zero padding at the end of the file.) my $FILESIZE = $DATASIZE + $BMPVER + 14; my $EOFPAD = (4 - (($FILESIZE) & 3)) & 3; $DATASIZE += $EOFPAD; my $HEADER = MakeBMPHeader($W, $H, $DOUT << 3, $COMPR, $BMPVER, $DATASIZE, 0, 0, '', 720, $AMASK, $RMASK, $GMASK, $BMASK) or return 0; # This is an explanation of the arguments for MakeBMPHeader(): # First, we pass the image width and image height, followed by the # Bits Per Pixel ($DOUT << 3), Compression value, BMP Version, # Size of the entire image in bytes ($DATASIZE), Number of colors # and important colors are zero (these values have no significance # when truecolor images are saved), the palette is an empty string, # because truecolor images do not use a palette). We use 720 DPI # as the recommended print resolution. Finally, we include the bit # masks ($AMASK, $RMASK, $GMASK, $BMASK) which specify how the # R G B A channels are stored inside each pixel. # Make some calculations... my $INPUT_ROWLEN = $W * $DIN; my $OUTPUT_ROWLEN = $W * $DOUT; my $PADLEN = (4 - ($OUTPUT_ROWLEN & 3)) & 3; my $PADDING = "\0" x $PADLEN; my $PTR = ($H - 1) * $INPUT_ROWLEN + $START; # Pointer to last row my $STEP = -($INPUT_ROWLEN * 2); # to jump to prev row # Enumerate each format so we can later identify them quickly. my $AX = CountBits32($AMASK); my $RX = CountBits32($RMASK); my $GX = CountBits32($GMASK); my $BX = CountBits32($BMASK); my $f = $AX << 16 | $RX << 12 | $GX << 8 | $BX << 4 | $DOUT; my ($PX, $A, $R, $G, $B, $PIXEL) = (0) x 6; # Print some stuff... $| = 1; print "\n\nSaving BMP file: $F ( $W x $H )", "\nUsing ", ($DOUT << 3), "-bit $ENCODING encoding.\n"; printf("\n AMASK = %0.8x\tPADDING = $PADLEN\t ORIENTATION = UPSIDE +DOWN", $AMASK); printf("\n RMASK = %0.8x\tBMPVER = $BMPVER\t PTR = $PTR", $RMASK); printf("\n GMASK = %0.8x\tCOMPRESSION = $COMPR\t STEP = $STEP", $GM +ASK); printf("\n BMASK = %0.8x\tHEADERLEN = ", $BMASK); print length($HEADER), "\t MAXCOLORS = $MAXCOLORS\n..."; # Create BMP file... local *FILE; open(FILE, ">$F") or return 0; binmode FILE; print FILE $HEADER; undef $HEADER; # And now, let's encode the pixels. while ($H--) { for (my $X = 0; $X < $W; $X++) { # Read pixels from canvas if ($DIN == 1) # Use 8-bit input source { my $CX = vec($$CANVAS, $PTR++, 8) << 2; $A = vec($PALETTE, $CX++, 8); $R = vec($PALETTE, $CX++, 8); $G = vec($PALETTE, $CX++, 8); $B = vec($PALETTE, $CX++, 8); } else # Use 24-bit or 32-bit input source { $A = vec($$CANVAS, $PTR++, 8) if ($DIN == 4); $R = vec($$CANVAS, $PTR++, 8); $G = vec($$CANVAS, $PTR++, 8); $B = vec($$CANVAS, $PTR++, 8); } # Convert pixel to appropriate output format: if ($f == 0x88884) { $PIXEL = pack('CCCC', # A8 R8 G8 B8 $B, $G, $R, $A); } elsif ($f == 0x08884) { $PIXEL = pack('CCCC', # X8 R8 G8 B8 $B, $G, $R, 0); } elsif ($f == 0x08883) { $PIXEL = pack('CCC', # R8 G8 B8 $B, $G, $R); } elsif ($f == 0x05652) { $PIXEL = pack('CC', # R5 G6 B5 (($B >> 3) & 0x1F) | (($G << 3) & 0xE0), ($R & 0xF8) | (($G & 0xE0) >> 5)); } elsif ($f == 0x15552) { $PIXEL = pack('CC', # A1 R5 G5 B5 (($G << 2) & 0xE0) | ($B >> 3), (($R >> 1) & 0x7C) | ($G >> 6) | ($A & 0x80)); } elsif ($f == 0x05552) { $PIXEL = pack('CC', # X1 R5 G5 B5 (($G << 2) & 0xE0) | ($B >> 3), (($R >> 1) & 0x7C) | ($G >> 6)); } elsif ($f == 0x44442) { $PIXEL = pack('CC', # A4 R4 G4 B4 ($G & 0xF0) | ($B >> 4), ($A & 0xF0) | ($R >> 4)); } elsif ($f == 0x04442) { $PIXEL = pack('CC', # X4 R4 G4 B4 ($G & 0xF0) | ($B >> 4), ($R >> 4)); } elsif ($f == 0x88882) { $PIXEL = pack('CC', # Grayscale A8 L8 int(($R + $G + $B) / 3 + 0.5), $A); } print FILE $PIXEL; # Write pixel # After writing 3000 pixels, we print one dot to stdout. $PX++ < 3000 or $PX = print '.'; } $PTR += $STEP; # Move to beginning of next line if ($PADLEN) { print FILE $PADDING; } # Add padding if needed } print FILE "\0" x $EOFPAD; # Make Adobe Photoshop happy. close FILE; print "\nDONE.\n"; return 1; } # # BMP | Graphics | v2022.11.24 # This function reads a BMP file's header and returns # a short list of values. The first value is the image # width, followed by the image height, depth, a format # string, and an error code. If the error code is # non-zero, then the file either doesn't exist or # is not a BMP file or has some other serious # issue which will prevent it from being displayed. # # Usage: (W, H, D, FMT, ERR) = QuickReadBMPHeader(FILENAME) # sub QuickReadBMPHeader { # Read the first 70 bytes from the file. my $HEADER = ''; my $F = FilterFileName($_[0]); my $ERR = ReadFile($_[0], $HEADER, 0, 70); if ($ERR) { return (0, 0, 0, '', $ERR); } vec($HEADER, 70, 8) = 0; # Expand header if it's too short. # Unpack header values. 54 my ($SIG, $VER, $W, $H, $BPP, $COMPR, $RX, $GX, $BX, $AX) = unpack('v x12 V3 xx vV x20 V4', $HEADER); if ($VER < 16) { ($W, $H, $BPP) = unpack('x18 vvxxv', $HEADER); $COMPR = 0; } # Check BMP Version number. index('12|16|40|52|56|58|64|108|124|', "$VER|") >= 0 or $ERR |= 4; # Check bits per pixel value. index('1|4|8|16|24|32|', "$BPP|") >= 0 or $ERR |= 8; # Check compression value. index('0|1|2|3|', "$COMPR|") >= 0 or $ERR |= 16; # Check file signature. $SIG == 0x4D42 or $ERR |= 32; if ($ERR) { return (0, 0, 0, '', $ERR); } # Fix image height. if ($H & 0x80000000) { $H = NEG32($H); } # Check image width and height. $W && $H or $ERR |= 64; # Color indexed BMP file? Exit here: if ($BPP <= 8) { return ($W, $H, $BPP, '', $ERR); } # Build format string. if ($COMPR == 3 && $VER >= 56) { $AX = CountBits32($AX); $RX = CountBits32($RX); $GX = CountBits32($GX); $BX = CountBits32($BX); } else { $AX = ($BPP < 32) ? 0 : 8; $RX = $GX = $BX = 8; } my $UX = $BPP - $AX - $RX - $GX - $BX; $UX = ($UX) ? "X$UX-" : ''; $AX = ($AX) ? "A$AX-" : ''; my $FMT = $UX . $AX . "R$RX-G$GX-B$BX"; return ($W, $H, $BPP, $FMT, 0); } # This function returns a complete BMP file header # which is usually between 50 and 1100 bytes long. # Returns an empty string if something goes wrong. # # Usage: HEADER = MakeBMPHeader(WIDTH, HEIGHT, BPP, # COMPR, BMPVER, DATASIZE, COLORS, IC, PALETTE, # DPI, AMASK, RMASK, GMASK, BMASK) # sub MakeBMPHeader { @_ >= 5 or return ''; my ($W, $H, $BPP, $COMPR, $BMPVER, $DATASIZE, $COLORS, $IC, $PALETTE, $DPI, $AMASK, $RMASK, $GMASK, $BMASK) = @_; # Fix some errors. $BMPVER = NearestNum($BMPVER, 12, 16, 40, 52, 56, 64, 108, 124); my $PALMAX = ($BMPVER < 16) ? 768 : 1024; if (length($PALETTE) == 1024) { if ($BMPVER < 16) { ReorderStr(\$PALETTE, 4, '432'); } else { ReorderStr(\$PALETTE, 4, '4321'); } } if (length($PALETTE) > $PALMAX) { $PALETTE = substr($PALETTE, 0, $PALMAX); } if ($BPP >= 16 && $BMPVER >= 56) { $COMPR = 3; } # Check limitations. if ($BMPVER < 40 && ($W > 65535 || $H > 65535)) { $BMPVER = 40; } if ($W > 4294967295) { print "\nBMP image width cannot exceed 4,294,967,295 pixels!\n"; return ''; } if ($H > 2147483647) { print "\nBMP image height cannot exceed 2,147,483,647 pixels!\n"; return ''; } # Colors and Important Colors (IC) have significance when we're # working with color-indexed images. A zero value means # that all colors are used and all colors are important. # In most BMP files, both COLORS and IC are zero. my $MAXCOLORS = GetMaxColors($BPP); FixOverflow($COLORS, $MAXCOLORS, 0); FixOverflow($IC, $MAXCOLORS, 0); # It is okay for DATASIZE and FILESIZE to be zero, # because most programs ignore these values anyway. # (When DATASIZE is zero, it has a special meaning, but # that only comes into play when using RLE compression.) # We add 14, because that's the primary header size. my $HDRSIZE = 14 + $BMPVER + length($PALETTE); my $FILESIZE = $HDRSIZE + $DATASIZE; FixOverflow($DATASIZE, 4294967295, 0); FixOverflow($FILESIZE, 4294967295, 0); # XRES and YRES hold the recommended print resolution. # (It's perfectly fine to leave these values zero.) my $XRES = int($DPI * 3.9365); my $YRES = int($DPI * 3.9365); # Assemble BMP Header. my $HEADER = 'BM' . pack(($BMPVER < 16 ? 'V4v4' : 'V6vv'), $FILESIZE, 0, $HDRSIZE, $BMPVER, $W, $H, 1, $BPP); if ($BMPVER > 16) { $HEADER .= pack('V6', $COMPR, $DATASIZE, $XRES, $YRES, $COLORS, $IC); } if ($BPP >= 16 && $COMPR == 3 && $BMPVER >= 56 && $HDRSIZE >= 70) { $HEADER .= pack('V4', $RMASK, $GMASK, $BMASK, $AMASK); } elsif ($BPP <= 8) { $HEADER .= $PALETTE; } if (length($HEADER) < $HDRSIZE) { $HEADER .= "\0" x ($HDRSIZE - length($HEADER)); } # Fill the rest with zeros. return $HEADER; } # This function converts a color from integer to string. # # Usage: STRING = toColorStr(COLOR, DEPTH) # sub toColorStr { my $C = Int32bit($_[0]); my $D = GetBPP($_[1]); if ($D == 1) { return chr($C & 255); } if ($D == 2) { return pack('n', $C & 0xffff); } if ($D == 4) { return pack('N', $C & 0xffffffff); } if ($D == 3) { return substr(pack('N', $C & 0xffffff), 1, 3); } return ''; } # This function splits up a 24-bit integer and returns # the individual R-G-B color values as integers. # Example: (0xff, 0xbb, 0x99) = Int2RGB(0xffbb99); # Usage: (R, G, B) = Int2RGB(INTEGER) # sub Int2RGB { defined $_[0] or return (0, 0, 0); my $R = ($_[0] >> 16) & 255; my $G = ($_[0] >> 8) & 255; my $B = $_[0] & 255; return ($R, $G, $B); } # Converts a 24-bit integer to a 3-byte string. # Usage: STRING = ColorStr(INTEGER) # sub ColorStr { my $C = "\0\0\0"; defined $_[0] or return $C; vec($C, 0, 8) = ($_[0] >> 16) & 255; # Write RED vec($C, 1, 8) = ($_[0] >> 8) & 255; # Write GRN vec($C, 2, 8) = $_[0] & 255; # Write BLU return $C; } # # Math | v2022.12.21 # This function converts a number to a 32-bit integer. # If the number is undefined to begin with, then # it returns the default value which is provided # in the second argument. If the default value # is undefined, then returns zero. # # Usage: INTEGER = Int32bit(NUMBER, [DEFAULT]) # sub Int32bit { no warnings; my $DEF = defined $_[1] ? $_[1] & 0xffffffff : 0; my $INT = defined $_[0] ? $_[0] & 0xffffffff : $DEF; use warnings; return $INT; } # # Math | v2022.11.5 # This function checks if a value is above and # beyond a certain limit, and if it is, then it # overwrites the first argument's value with the # third argument's value. Returns the final new value. # # Also, if the first argument is undefined, # it overwrites it with zero! # # Usage: NUMBER = FixOverflow(VARIABLE, LIMIT, NEWVALUE) # sub FixOverflow { defined $_[0] or return $_[0] = 0; no warnings; my $NEW = defined $_[2] ? $_[2] : 0; if (defined $_[1] && $_[0] > $_[1]) { $_[0] = $NEW; } use warnings; return $_[0]; } # # Math | v2022.11.5 # This function expects a list of numbers and decides # which one is closest to the first one and returns that number. # Returns the number itself if the list is empty. # # Example: NearestNum(25, 55, 35, 99) => 35 # NearestNum(88, 90, 88, 77, 14) => 88 # NearestNum(103) => 103 # # Usage: NUMBER = NearestNum(FIRST_NUMBER, LIST OF NUMBERS...) # sub NearestNum { my $FIRST = shift; my $NEAREST = $FIRST; my $LEASTDIFF = 999999999999999; foreach (@_) { my $DIFF = abs($FIRST - $_); $DIFF or return $FIRST; if ($LEASTDIFF > $DIFF) { $LEASTDIFF = $DIFF; $NEAREST = $_; } } return $NEAREST; } # # Graphics | v2022.11.5 # This function calculates the maximum possible # colors based on the bit per pixel value. # # Usage: INTEGER = GetMaxColors(BPP) # sub GetMaxColors { my $BPP = defined $_[0] ? $_[0] : 0; $BPP > 0 or return 0; $BPP >= 24 or return 1 << 8; return 16777216; } # # String | v2022.10.18 # This function removes whitespace from before and # after STRING. Whitespace is here defined as any # character whose ASCII value is less than 33. # This includes spaces, tabs, esc, null, new lines, etc. # Usage: STRING = Trim(STRING) # sub Trim { defined $_[0] or return ''; (my $L = length($_[0])) or return ''; my $P = 0; while ($P <= $L && ord(substr($_[0], $P++, 1)) < 33) {} $P--; # Fin +d first non-whitespace while ($P <= $L && ord(substr($_[0], $L--, 1)) < 33) {} # Fin +d last non-whitespace return substr($_[0], $P, $L - $P + 2); } # # Math | v2022.11.19 # This function converts a 32-bit integer to a # binary number that consists of 1s and 0s, and # counts the number of zeroes that are at the end # of the number. (Actually, we use a lookup table # to speed things up a bit...) # # Example: ZeroCountR32(1500000) => 5 # # 1500000 = 00000000000101101110001101100000 # ^^^^^ # 5 # Usage: COUNT = ZeroCountR32(INTEGER) # sub ZeroCountR32 { my $N = $_[0] & 0xffffffff; $N or return 32; my $HI = ZeroCountR16($N >> 16); my $LO = ZeroCountR16($N); return ($LO < 16) ? $LO : $HI + 16; } # # Math | v2022.11.19 # This function converts an integer (0-65535) to a # 16-digit number that consists of 1s and 0s, and # counts the number of zeroes that are on the right # side of that number. (Actually, we use a lookup # table to speed things up a bit.) # # Example: ZeroCountR16(696) => 3 # # 696 = 0000001010111000 # ^^^ # 3 # # Usage: COUNT = ZeroCountR16(INTEGER) # sub ZeroCountR16 { defined $_[0] or return 16; my $N = $_[0] & 0xffff; $N or return 16; # Let me guess...it's zero? my @HI = ZeroCount8($N >> 8); my @LO = ZeroCount8($N); return ($LO[1] < 8) ? $LO[1] : $HI[1] + 8; } # # Math | v2022.11.19 # This function converts an integer (0-255) to an # 8-digit number that consists of 1s and 0s, and # counts the number of zeroes that come before # and after the number. (Actually, we use a # lookup table to speed things up a bit.) # # Example: ZeroCount8(40) => (2, 3) # # 40 = 00101000 # ^^ ^^^ # 2 3 # # The second and third arguments are optional: # The second argument will be added to BEFORE's value. # The third argument will be added to AFTER's value. # # Usage: (BEFORE, AFTER) = ZeroCount8(INTEGER, [ADD1, [ADD2]]) # sub ZeroCount8 { # DO NOT MODIFY LOOKUP TABLE: my $N = vec("\xB0\xA8\x97\x98\x86\x88\x87\x88uxwxvxwxdhghfhghehghfhg +hSXWXVXWXUXWXVXWXTXWXVXWXUXWXVXWXBHGHFHGHEHGHFHGHDHGHFHGHEHGHFHGHCHGH +FHGHEHGHFHGHDHGHFHGHEHGHFHGH18786878587868784878687858786878387868785 +878687848786878587868782878687858786878487868785878687838786878587868 +784878687858786878", $_[0] & 255, 8); my $BEFORE = ($N >> 4) + (defined $_[1] ? $_[1] : 0) - 3; my $AFTER = (8 - ($N & 15)) + (defined $_[2] ? $_[2] : 0); return ($BEFORE, $AFTER); } # # String | v2018.6.5 # This function inserts commas into a number at # every 3 digits and returns a string. # Usage: STRING = Commify(INTEGER) # Copied from www.PerlMonks.org/?node_id=157725 # sub Commify { my $N = reverse $_[0]; $N =~ s/(\d\d\d)(?=\d)(?!\d*\.)/$1,/g; return scalar reverse $N; }