Beefy Boxes and Bandwidth Generously Provided by pair Networks
Perl: the Markov chain saw
 
PerlMonks  

Cannot Set Image Pixel to a Random Colour using GD

by ozboomer (Friar)
on Dec 24, 2022 at 11:05 UTC ( [id://11149054]=perlquestion: print w/replies, xml ) Need Help??

ozboomer has asked for the wisdom of the Perl Monks concerning the following question:

Hi again, folks...

The ultimate objective of the exercise here is to modify a truecolour image so the saturation of each pixel in the image is reduced.

With the following attempt, I'm trying to use GD to see what each pixel's RGB value is, change the value and set the pixel to the new RGB value.

use strict; use warnings; use GD; use Imager; # doesn't fail on non-existent file?... #my $image = GD::Image->newFromPng("rudddy.png") || die("cant on loadi +ng file\n"); my $image = GD::Image->newFromPng("ruddy.png") || die("cant on loading + file\n"); $image->trueColor(1); my ($max_X, $max_Y)=$image->getBounds(); my $maxColors = $image->colorsTotal; my $X0 = int($max_X / 3); my $Y0 = int($max_Y / 3); for (my $x = $X0; $x < $X0+5; $x++) { for (my $y = $Y0; $y < $Y0+5; $y++) { #for (my $x = 0; $x < $max_X; $x++) { # for (my $y = 0; $y < $max_Y; $y++) { printf(" PIXEL: X=%03d Y=%03d\n", $x, $y); my $org_colour = $image->getPixel($x, $y); my @rgb = $image->rgb($org_colour); printf("BEFORE: colour idx=%5d R=%3d, G=%3d, B=%3d\n", $org_colo +ur, @rgb); # Update_Saturation(\@rgb, \my @new_rgb); # REAL transformation @rgb[1] = 0; # ### TESTING ### transformation my @new_rgb = @rgb; # my $new_colour = $image->colorAllocate(@new_rgb); # Generates + error; result = -1 my $new_colour = $image->colorClosest(@new_rgb); # Non-accur +ate colours $image->setPixel($x, $y, $new_colour); printf(" AFTER: colour idx=%5d R=%3d, G=%3d, B=%3d\n\n", $new_co +lour, @new_rgb); } # end: another row (y) } # end: another column (x) open(my $fh, ">", "new.png") || die("cant on output file\n"); binmode $fh; print $fh $image->png; exit(0); # ------------------------------------- # Update_Saturation - Reduce saturation of a pixel # Uses Globals: # ------------------------------------- sub Update_Saturation { my ($in_arr_ref, $out_arr_ref) = @_; my $colour = Imager::Color->new(@$in_arr_ref); my @hsv = $colour->hsv(); my ($h, $s, $v) = @hsv; $s = 0.7 * $s; my $new_colour = Imager::Color->new(hue => $h, saturation => $s, va +lue => $v); my ($red, $green, $blue, $alpha) = $new_colour->rgba(); @$out_arr_ref = ($red, $green, $blue); return(1); } # end Update_Saturation

I'm a bit vague on the intricacies of GD... but I think I understand that the colour map for the image is used... So, that means a random/undefined colour (RGB) value wouldn't be found in the colour map, hence something like GD::Image->colorAllocate() would fail, which it does.

So, maybe there's a way to do this... or maybe I should be using something else to manipulate each pixel. I'm trying to avoid dealing with ImageMagick (at least within Perl), owing to its learning curve and 'difficulties' I'll have with installing it on my PC.

Using ActiveState Perl v5.20.2 under Windoze 8.1 32-bit and GD v2.53... if anyone can offer some clues on where to go next.

Thanks a heap.

Replies are listed 'Best First'.
Re: Cannot Set Image Pixel to a Random Colour using GD
by Fletch (Bishop) on Dec 24, 2022 at 13:57 UTC

    Handwavy (early and my morning caffeine hasn't kicked in) but if you're trying to uniformly desaturate all colors what I'd do would be to walk the pallet instead (not swapping the image to true color of course):

    • Get the total number of pallet entries
    • For each index 0..$num_colors
      • Fetch the rgb for the index
      • colorDeallocate that index
      • Compute your new desaturated color and then colorAllocate it back (which should (?) put it back in the recently freed index)

    The cake is a lie.
    The cake is a lie.
    The cake is a lie.

      An interesting, probably slicker approach... but I wonder if there may be something like a 'refresh' of the palette required?

      As it stands, I don't think I'm dealing with a 'palette-ed' image here:-

      c:\> ffprobe -hide_banner ruddy.png Input #0, png_pipe, from 'ruddy.png': Duration: N/A, bitrate: N/A Stream #0:0: Video: png, rgb24(pc), 320x234, 25 fps, 25 tbr, 25 tbn c:\> mediainfo ruddy.png General Complete name : ruddy.png Format : PNG Format/Info : Portable Network Graphic File size : 84.9 KiB Image Format : PNG Format/Info : Portable Network Graphic Width : 320 pixels Height : 234 pixels Bit depth : 24 bits Compression mode : Lossless Stream size : 84.9 KiB (100%)
Re: Cannot Set Image Pixel to a Random Colour using GD
by Anonymous Monk on Dec 24, 2022 at 16:33 UTC

    I assume your intent is to open and save "true colour" RGB, 8 bits per channel images, as opposed to paletted images. "Learning curve" or not, you'll get nowhere before you understand the difference, so as not to try to use methods designed for paletted images on "true colour" images.

    trueColor is class method; GD documentation is clear about that. However, it also says that, by default, "true colour" GD object is created if PNG already was "true colour". It's not what I observe. Instead of calling class method before opening anything, I'd suggest providing an explicit flag to constructor, as in example below.

    Using Imager just to desaturate triplets of values, while at the same time using GD to manipulate an image is so wrong on all sides, that I simply chose to use another distribution for the purpose. The Convert::Color and Colouring::In are what searching the CPAN reveals (the latter even provides the desaturate method); but you can continue to use Imager, of course. If it's all just for exercise, it may (or may not) be beneficial to write RGB to HSV (and back) conversions yourself. If it's not just an exercise, I'd strongly advise not to get and set pixels one by one in high level language.

    Note that getPixel returns large integer in case of "true color", which is just packed BGR triplet. The "convert" is ImageMagick's executable supposed to be in the PATH. + Actually calling reverse twice isn't required to only mess with saturation alone.

    use strict; use warnings; use feature 'say'; use open IO => ':raw'; use GD; use Convert::Color; sub desaturate { local $" = ','; my @HSV = Convert::Color-> new( "rgb8:@_" )-> as_hsv-> hsv; $HSV[ 1 ] *= 0.7; return Convert::Color-> new( "hsv:@HSV" )-> as_rgb8-> rgb8 } my $gd = GD::Image-> newFromPngData( scalar qx/convert rose: png:-/, 1 + ); for my $y ( 0 .. $gd-> height - 1 ) { for my $x ( 0 .. $gd-> width - 1 ) { my $packed_rgb = $gd-> getPixel( $x, $y ); my @RGB = reverse unpack 'C3', pack 'L', $packed_rgb; $packed_rgb = unpack 'L', pack 'C3x', reverse desaturate( @RGB + ); $gd-> setPixel( $x, $y, $packed_rgb ) } } open my $fh, '>', 'desaturated_rose.png' or die; binmode $fh; print $fh $gd-> png; close $fh;

      As I said, I'm admittedly pretty vague on GD... so I was just having a go with the tools I know about.

      Convert::Color was something I'd not come across in my explorations... so that's been added to the toolkit. I'm pretty new at manipulating graphics (beyond simply getting size information and calling ffmpeg and similar CLI tools from within my code)... so I'm feeling my way a lot..

      Further down the track, I'm trying to see if I can automatically 'tidy-up' some 100s (or 1000s) of smaller images so they all 'look' the same.. without going to the hassle of trying to learn about 'computer vision', OpenCV and such unless I really have to... particularly when I can conceive of a simpler way, even if it's slower or 'inelegant'(!).

      I've modified the suggested code a bit so it works on my system (and I can understand it a bit more easily):-

      use strict; use warnings; # unused ... use feature 'say'; # unnecessary? ... use open IO => ':raw'; use GD; use Convert::Color; # Handy.. Didn't know of this module my $saturation_factor = 0.9; # Will ultimately be coming from elsewhe +re # ---- sub desaturate { local $" = ','; my @HSV = Convert::Color-> new( "rgb8:@_" )-> as_hsv-> hsv; $HSV[ 1 ] *= $saturation_factor; return Convert::Color-> new( "hsv:@HSV" )-> as_rgb8-> rgb8 } # ---- # this is as clear as mud in a beer bottle to me... # my $gd = GD::Image-> newFromPngData( scalar qx/convert rose: png:-/, + 1 ); my $gd = GD::Image->newFromPng("ruddy.png", 1) || die("cant on loading + file\n"); # 1 = truecolour # my ($max_X, $max_Y) = $gd->getBounds(); # My version my $max_X = $gd->width; # TMTOWTDI my $max_Y = $gd->height; #for my $y ( 0 .. $gd->height - 1 ) { # Construct causes 'unknown + method' errors ()... # for my $x ( 0 .. $gd->width - 1 ) { for my $y ( 0 .. $max_Y - 1 ) { for my $x ( 0 .. $max_X - 1 ) { my $packed_rgb = $gd-> getPixel( $x, $y ); my @RGB = reverse unpack 'C3', pack 'L', $packed_rgb; $packed_rgb = unpack 'L', pack 'C3x', reverse desaturate( @RGB + ); $gd-> setPixel( $x, $y, $packed_rgb ) } } open my $fh, '>', 'new.png' or die; binmode $fh; print $fh $gd->png; close $fh;

      Initially, I'm thinking about looking at (relative) luminance as a measure that I can try and match between images... so this code is only a step in the whole process.

Re: Cannot Set Image Pixel to a Random Colour using GD
by harangzsolt33 (Chaplain) on Dec 26, 2022 at 14:19 UTC
    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; }

Log In?
Username:
Password:

What's my password?
Create A New User
Domain Nodelet?
Node Status?
node history
Node Type: perlquestion [id://11149054]
Approved by hippo
Front-paged by Corion
help
Chatterbox?
and the web crawler heard nothing...

How do I use this?Last hourOther CB clients
Other Users?
Others meditating upon the Monastery: (9)
As of 2024-04-18 10:11 GMT
Sections?
Information?
Find Nodes?
Leftovers?
    Voting Booth?

    No recent polls found