```{index} single: GD; raster drawing single: libgd ``` # Raster drawing with GD `GD` is Perl's binding to libgd, the small C library behind a great deal of the web's generated images: thumbnails, charts, captchas, badges. It creates an image, draws on it, filters it, and saves it, and its drawing primitives are the ones from the [algorithms chapter](raster-algorithms) running at C speed. On pperl it is a native module: the C half is bound directly to the system's libgd, and GD's own Perl half (`GD::Image`, `GD::Polygon`, `GD::Simple`) runs as it is, so code written for `GD` on any Perl 5 runs here unchanged. The API is positional and terse. Methods take coordinates and a colour in a fixed order, and most of them return nothing useful. The ones that can fail either return false or `undef`, and the idiom is to check those, or die with the reason, as the file loaders do. ```perl use GD; my $im = GD::Image->new(400, 300, 1); # width, height, true-colour ``` ```{index} single: GD; colours single: GD; colorAllocate single: GD; colorResolve single: palette image single: true-colour image ``` ## Colours `GD`'s defining quirk is how it names a colour. You never pass red, green and blue to a drawing method. You ask the image for a colour first, with `colorAllocate`, and then draw with the integer it returns. What that integer means depends on the kind of image. A **palette** image (created without the third argument) holds a table of at most 256 colours, and every pixel stores an index into it. This is the model of GIF and of 256-colour displays. `colorAllocate` adds an entry and returns its index, and the first colour you allocate becomes the background. A **true-colour** image (third argument `1`) stores the colour itself in every pixel. `colorAllocate` has no table to add to, so it simply packs the channels into one integer, and that integer is the colour. Prefer true-colour for all new work: it has no 256-colour ceiling, and filters, resampling and alpha blending all assume it. ```perl use strict; use warnings; use GD; # a palette image: colorAllocate hands out table indices my $pal = GD::Image->new(100, 100); my $white = $pal->colorAllocate(255, 255, 255); # first one is the background my $red = $pal->colorAllocate(255, 0, 0); print "palette: white=$white red=$red, ", $pal->colorsTotal, " colours in the table\n"; # a truecolor image: the "index" is the colour itself, packed my $tc = GD::Image->new(100, 100, 1); my $tw = $tc->colorAllocate(255, 255, 255); my $tr = $tc->colorAllocate(255, 0, 0); my $half = $tc->colorAllocateAlpha(255, 0, 0, 64); # about 50% transparent printf "truecolor: white=0x%08x red=0x%08x half=0x%08x\n", $tw, $tr, $half; ``` ```text palette: white=0 red=1, 2 colours in the table truecolor: white=0x00ffffff red=0x00ff0000 half=0x40ff0000 ``` On a palette image the table does fill up. The 257th `colorAllocate` returns `-1`, and `colorResolve` is the safe alternative: it returns an exact match if there is one, allocates if there is room, and otherwise falls back to the closest colour already in the table. ```perl use strict; use warnings; use GD; my $pal = GD::Image->new(10, 10); $pal->colorAllocate($_, 0, 0) for 0 .. 255; # fill all 256 slots print "table: ", $pal->colorsTotal, " colours\n"; print "allocate one more: ", $pal->colorAllocate(1, 2, 3), "\n"; print "resolve instead: ", $pal->colorResolve(1, 2, 3), "\n"; ``` ```text table: 256 colours allocate one more: -1 resolve instead: 1 ``` `colorExact` and `colorClosest` are the two halves of `colorResolve` on their own; `rgb($colour)` turns an index or packed colour back into its three channels. ```{index} single: GD; alpha single: GD; alphaBlending single: alpha channel; in GD ``` ### Alpha, the GD way `colorAllocateAlpha` takes a fourth value, and it runs the opposite way to the [foundations chapter](foundations): in `GD`, alpha `0` is fully **opaque** and `127` is fully **transparent**, a 7-bit range with transparency at the top. So `64` is roughly half and half. What happens when you draw with a translucent colour depends on `alphaBlending`. With blending on (the default for true-colour images) the colour is composited source-over onto what is already there. With it off, the pixel is simply replaced, alpha and all. ```perl use strict; use warnings; use GD; my $im = GD::Image->new(100, 100, 1); my $white = $im->colorAllocate(255, 255, 255); my $half = $im->colorAllocateAlpha(255, 0, 0, 64); my @before = $im->rgb($im->getPixel(50, 50)); print "fresh canvas: @before\n"; # truecolor starts black $im->filledRectangle(0, 0, 99, 99, $white); $im->filledRectangle(0, 0, 49, 99, $half); # blending on: composite $im->alphaBlending(0); $im->filledRectangle(50, 0, 99, 99, $half); # blending off: replace for my $x (25, 75) { my $c = $im->getPixel($x, 50); printf "x=%d: rgb=%s alpha=%d\n", $x, join(' ', $im->rgb($c)), $im->alpha($c); } ``` ```text fresh canvas: 0 0 0 x=25: rgb=255 128 128 alpha=0 x=75: rgb=255 0 0 alpha=64 ``` The blended half is the pink `255 128 128` the foundations chapter computed by hand for red at half opacity over white. The other half keeps pure red with alpha `64` stored in the pixel, which is what you want when you are building an image with transparent regions to save (see [transparency in files](#transparency-in-files) below). The first line of output is a trap worth stating once: a new true-colour image starts **black**, not white. Always paint the background with a `filledRectangle` over the whole canvas before drawing anything else. ```{index} single: GD; drawing primitives single: GD::Polygon single: flood fill; in GD ``` ## The primitives Every primitive takes its geometry first and the colour last, all positional. Rectangles use two corners, both inclusive, so a full 400 x 300 canvas is `(0, 0, 399, 299)`. Ellipses and arcs use a centre plus the **full width and height** (not a radius), and arc angles are in **degrees**, starting at three o'clock and increasing clockwise (clockwise because y grows downward). ```perl use strict; use warnings; use GD; my $im = GD::Image->new(400, 300, 1); my $white = $im->colorAllocate(255, 255, 255); my $black = $im->colorAllocate(0, 0, 0); my $blue = $im->colorAllocate(0, 90, 200); my $red = $im->colorAllocate(220, 40, 40); my $green = $im->colorAllocate(60, 170, 80); my $yellow = $im->colorAllocate(250, 200, 40); $im->filledRectangle(0, 0, 399, 299, $white); # background # lines: plain, dashed, and thick $im->line(10, 10, 130, 60, $black); $im->dashedLine(10, 30, 130, 80, $black); $im->setThickness(5); $im->line(10, 60, 130, 110, $blue); $im->setThickness(1); # rectangles: corner to corner, both corners inclusive $im->rectangle(150, 10, 250, 80, $black); $im->filledRectangle(270, 10, 390, 80, $yellow); # ellipses and arcs: centre, then full width and height $im->ellipse(70, 170, 110, 60, $black); $im->filledEllipse(70, 170, 50, 50, $blue); $im->arc(200, 170, 100, 100, 0, 270, $black); # three quarters $im->filledArc(330, 170, 100, 100, 30, 330, $yellow); # a pie slice $im->filledArc(330, 170, 100, 100, 30, 330, $black, gdEdged | gdNoFill); # polygons come from a GD::Polygon my $tri = GD::Polygon->new; $tri->addPt(20, 290); $tri->addPt(120, 290); $tri->addPt(70, 220); $im->filledPolygon($tri, $green); $im->polygon($tri, $black); my $zig = GD::Polygon->new; $zig->addPt($_->[0], $_->[1]) for [150, 280], [180, 230], [210, 280], [240, 230]; $im->unclosedPolygon($zig, $red); # open path, no closing edge # flood fills $im->rectangle(270, 230, 390, 290, $black); $im->line(330, 230, 330, 290, $black); $im->fill(300, 260, $red); # fills the left cell only $im->fillToBorder(360, 260, $black, $blue); # stops at black pixels open my $fh, '>', 'primitives.png' or die "primitives.png: $!"; binmode $fh; print $fh $im->png; close $fh; ``` The result is a 3 x 3 sheet: three lines, an outlined and a filled rectangle; an ellipse around a disc, a three-quarter arc open at the top right, a pie with its mouth to the right; a filled triangle, a red zigzag, and a box whose two halves were flood-filled red and blue. A few details the sheet shows: - `filledArc` takes an optional style. The default fills a pie slice; `gdChord` replaces the curved edge with a straight line between the two end points, `gdNoFill` draws only the outline, and `gdEdged` adds the two radii. They combine with `|`, which is how the pie above gets its black outline on a second call. - `polygon` (also spelled `openPolygon`) draws a closed outline; `unclosedPolygon` leaves off the edge from the last point back to the first, which is the polyline. `filledPolygon` fills using the [scanline method](raster-algorithms). - `fill` floods the connected region of the colour under the seed point, so it stopped at the black divider. `fillToBorder` floods until it meets one named border colour, whatever it crosses on the way. - `setThickness` changes the pen for every later line, outline and arc, until you set it back. Single pixels are `setPixel($x, $y, $colour)` and `getPixel($x, $y)`; the latter returns the index or packed colour, which `rgb` and `alpha` take apart. ```{index} single: GD; anti-aliasing single: anti-aliasing; in GD ``` ### Anti-aliasing Everything above is aliased: each pixel is fully on or fully off, the staircase of the [algorithms chapter](raster-algorithms). `GD` anti-aliases through a special colour. You name the real colour with `setAntiAliased`, then draw with the constant `gdAntiAliased` in place of a colour: ```perl use strict; use warnings; use GD; my $im = GD::Image->new(220, 60, 1); my $white = $im->colorAllocate(255, 255, 255); my $black = $im->colorAllocate(0, 0, 0); $im->filledRectangle(0, 0, 219, 59, $white); $im->line(10, 10, 100, 37, $black); # aliased $im->setAntiAliased($black); $im->line(120, 10, 210, 37, gdAntiAliased); # anti-aliased # the same column of each line, three pixels high for my $x (55, 165) { my @grey = map { ($im->rgb($im->getPixel($x, $_)))[0] } 22 .. 24; print "x=$x: @grey\n"; } open my $fh, '>', 'aa.png' or die "aa.png: $!"; binmode $fh; print $fh $im->png; close $fh; ``` ```text x=55: 255 255 0 x=165: 255 127 128 ``` At that column the ideal line passes exactly between two pixel rows. The aliased line picks one and paints it solid black; the anti-aliased line gives each row half coverage, two mid greys. That is the coverage idea from the algorithms chapter, computed for you. `gdAntiAliased` works for lines and for polygons, outlined or filled. `filledEllipse` ignores it and keeps a hard edge. For curves that must be smooth, see [when GD is the wrong tool](#when-gd-is-the-wrong-tool). `GD` has three more special colours in the same style: `gdStyled` draws with a repeating pattern of colours set by `setStyle` (a custom dash), `gdBrushed` stamps a small image set by `setBrush` along the line, and `gdTiled` fills with an image set by `setTile`. ```{index} single: GD; text single: GD; stringFT single: bitmap font single: TrueType font single: FreeType ``` ## Text `GD` has two text systems. The built-in **bitmap fonts** need nothing installed: `gdTinyFont`, `gdSmallFont`, `gdMediumBoldFont`, `gdLargeFont` and `gdGiantFont`, fixed-size and ASCII-oriented, drawn with `string` (and `stringUp` for text rotated 90 degrees). The **TrueType** route, `stringFT`, renders any font file through FreeType at any size and angle, anti-aliased. ```perl use strict; use warnings; use GD; my $im = GD::Image->new(320, 150, 1); my $white = $im->colorAllocate(255, 255, 255); my $black = $im->colorAllocate(0, 0, 0); my $blue = $im->colorAllocate(0, 90, 200); $im->filledRectangle(0, 0, 319, 149, $white); # the built-in bitmap fonts: x, y is the top-left of the text $im->string(gdSmallFont, 10, 10, 'gdSmallFont', $black); $im->string(gdGiantFont, 10, 28, 'gdGiantFont', $black); $im->stringUp(gdMediumBoldFont, 300, 140, 'stringUp', $blue); printf "gdGiantFont cell: %dx%d\n", gdGiantFont->width, gdGiantFont->height; # TrueType through FreeType: x, y is the baseline start my $font = '/usr/share/fonts/TTF/DejaVuSans.ttf'; my @box = $im->stringFT($blue, $font, 20, 0, 10, 90, 'TrueType text') or die "stringFT: $@"; print "bounding box: @box\n"; # angle is in radians, counter-clockwise $im->stringFT($black, $font, 11, 0.3, 180, 135, 'at an angle') or die "stringFT: $@"; # measure without drawing: call it on the class, not an image my @size = GD::Image->stringFT($black, $font, 20, 0, 0, 0, 'TrueType text'); printf "width %d, height %d\n", $size[2] - $size[0], $size[1] - $size[5]; # with fontconfig on, a font name works instead of a path $im->useFontConfig(1); $im->stringFT($black, 'DejaVu Sans:bold', 11, 0, 10, 135, 'by name') or die "stringFT: $@"; open my $fh, '>', 'text.png' or die "text.png: $!"; binmode $fh; print $fh $im->png; close $fh; ``` ```text gdGiantFont cell: 9x15 bounding box: 9 95 188 95 188 70 9 70 width 178, height 24 ``` The font path is a system detail; on your machine `fc-list` shows the installed fonts and their files, or turn on `useFontConfig` and name the font as the last call does. Three things differ between the two systems, and all three catch people. The **position**: `string` places the top-left corner of the text, `stringFT` places the start of the baseline, so the same `y` puts the two at different heights. The **argument order**: `string` takes the font first and the colour last, `stringFT` takes the colour first. And the **angle**: `stringFT` wants radians, counter-clockwise, while `arc` wants degrees, clockwise. `stringFT` returns the four corners of the text's bounding box as eight numbers (lower-left, lower-right, upper-right, upper-left, each as x and y), and an empty list on failure with the reason in `$@`, so with a wrong font path the example above dies with `stringFT: Could not find/open font`. Called on the class instead of an image it measures without drawing, which is how you centre or right-align a caption. ```{index} single: GD; saving and loading single: image formats; lossy and lossless ``` ## Saving and loading Output is one method per format, each returning the encoded bytes; you write them to a file opened in binary mode. ```perl use strict; use warnings; use GD; sub save { my ($file, $bytes) = @_; defined $bytes or die "$file: this libgd cannot write that format\n"; open my $fh, '>', $file or die "$file: $!"; binmode $fh; print $fh $bytes; close $fh; } my $im = GD::Image->new(200, 100, 1); my $white = $im->colorAllocate(255, 255, 255); my $red = $im->colorAllocate(200, 40, 40); $im->filledRectangle(0, 0, 199, 99, $white); $im->filledEllipse(100, 50, 160, 80, $red); save('shape.png', $im->png); # optional 0..9 compression level save('shape.jpg', $im->jpeg(85)); # quality 0..100 save('shape.webp', $im->webp(80)); # quality 0..100 save('shape.gif', $im->gif); # palette format: colours are reduced save('shape.bmp', $im->bmp); my @loaded = ( [ 'shape.png', GD::Image->newFromPng('shape.png', 1) ], # 1: true-colour [ 'shape.jpg', GD::Image->newFromJpeg('shape.jpg', 1) ], [ 'shape.webp', GD::Image->newFromWebp('shape.webp') ], [ 'shape.gif', GD::Image->newFromGif('shape.gif') ], [ 'shape.bmp', GD::Image->newFromBmp('shape.bmp') ], ); for (@loaded) { my ($file, $back) = @$_; printf "%-10s %5d bytes %dx%d centre=%s\n", $file, -s $file, $back->getBounds, join(' ', $back->rgb($back->getPixel(100, 50))); } ``` ```text shape.png 765 bytes 200x100 centre=200 40 40 shape.jpg 3216 bytes 200x100 centre=200 40 40 shape.webp 1274 bytes 200x100 centre=199 40 39 shape.gif 413 bytes 200x100 centre=204 42 44 shape.bmp 60054 bytes 200x100 centre=200 40 40 ``` The centre pixel tells the story of each format. PNG and BMP are lossless and give back exactly `200 40 40`. JPEG and WEBP are lossy; on a flat area like this they come back exact or within a step. GIF is a palette format, so the true-colour image was reduced to at most 256 colours on the way out, and the red moved to the nearest colour in that palette. Loading is one constructor per format, each taking a file name or an open filehandle: `newFromPng`, `newFromJpeg`, `newFromGif`, `newFromWebp`, `newFromBmp`. Each has a `Data` variant for bytes you already hold in a scalar, such as `newFromPngData($bytes)`. A loader either returns an image or dies: a missing file with `... not found`, a file it cannot decode with the name of the libgd call that refused it. That is why the loop above checks nothing. The second argument of `newFromPng` and `newFromJpeg` (and their `Data` variants) decides whether a true-colour file stays true-colour, and leaving it out has a cost: ```perl use strict; use warnings; use GD; my $pal = GD::Image->newFromPng('shape.png'); # no flag: palette my $tc = GD::Image->newFromPng('shape.png', 1); # 1: true-colour for ([ 'no flag' => $pal ], [ 'flag 1' => $tc ]) { my ($how, $im) = @$_; printf "%-8s isTrueColor=%d centre=%s\n", $how, $im->isTrueColor, join(' ', $im->rgb($im->getPixel(100, 50))); } my $was = GD::Image->trueColor(1); # the program-wide default my $any = GD::Image->new('shape.png'); printf "trueColor(1), was %d: new(file) isTrueColor=%d\n", $was, $any->isTrueColor; eval { GD::Image->newFromPng('shape.jpg') }; my ($why) = $@ =~ /^(.*?) at /; print "wrong format: $why\n"; ``` ```text no flag isTrueColor=0 centre=204 42 44 flag 1 isTrueColor=1 centre=200 40 40 trueColor(1), was 0: new(file) isTrueColor=1 wrong format: gdImageCreateFromPng error ``` Without the flag, `GD` converts the loaded file to a palette image, the same reduction the GIF went through, and the red moves. Pass `1` for anything you will filter, resample or blend, which is what the rest of this chapter assumes. `GD::Image->trueColor(1)` changes the default for the whole program and returns the previous one; it also makes `GD::Image->new($width, $height)` true-colour, and it is the only way to steer `GD::Image->new($file)`, which recognises PNG, JPEG, GIF and WEBP from the file's first bytes but takes no flag of its own. A palette file such as a GIF loads as a palette image either way, and `paletteToTrueColor` converts it in place. Which formats work is decided by how the system's libgd was built, not by `GD`. An encoder libgd was built without returns `undef`, which is why `save` above checks. `GD::supportsFileType` answers from libgd's format table, which lists a format as present even when its codec is a stub, so the reliable test is the one `save` does: ask for the bytes and see whether you got any. ```{index} single: GD; transparency single: transparency; PNG single: transparency; GIF ``` ### Transparency in files Two separate mechanisms make parts of a saved image transparent, one per image kind. ```perl use strict; use warnings; use GD; # a PNG with a real alpha channel my $im = GD::Image->new(100, 100, 1); $im->alphaBlending(0); # write alpha, do not blend $im->saveAlpha(1); # keep it in the PNG my $clear = $im->colorAllocateAlpha(0, 0, 0, 127); # fully transparent my $glass = $im->colorAllocateAlpha(0, 90, 200, 64); $im->filledRectangle(0, 0, 99, 99, $clear); $im->filledEllipse(50, 50, 80, 80, $glass); open my $fh, '>', 'glass.png' or die "glass.png: $!"; binmode $fh; print $fh $im->png; close $fh; # a palette GIF with one colour marked transparent my $gif = GD::Image->new(100, 100); my $bg = $gif->colorAllocate(255, 255, 255); my $ink = $gif->colorAllocate(0, 128, 0); $gif->transparent($bg); $gif->filledRectangle(25, 25, 74, 74, $ink); open $fh, '>', 'stamp.gif' or die "stamp.gif: $!"; binmode $fh; print $fh $gif->gif; close $fh; my $back = GD::Image->newFromPng('glass.png', 1); printf "glass.png: corner alpha %d, centre alpha %d\n", $back->alpha($back->getPixel(0, 0)), $back->alpha($back->getPixel(50, 50)); my $g = GD::Image->newFromGif('stamp.gif'); printf "stamp.gif: transparent index %d\n", $g->transparent; ``` ```text glass.png: corner alpha 127, centre alpha 64 stamp.gif: transparent index 0 ``` For a true-colour PNG, per-pixel alpha survives the round trip only when both switches are set: `alphaBlending(0)` so the alpha is written into the pixels rather than blended away, and `saveAlpha(1)` so the PNG encoder keeps the channel. Forget the second and the file comes out opaque. For a palette image, `transparent($index)` marks one colour of the table as "see-through", all or nothing, which is the only kind of transparency GIF has. ```{index} single: GD; scaling single: GD; copyResampled single: GD; copyMerge single: nearest-neighbour scaling single: resampling ``` ## Scaling and copying `GD` scales by copying from one image into another of a different size. You create the destination first, then copy a rectangle of the source into a rectangle of the destination. There are two copies, and the difference is the one between the aliased and anti-aliased lines. `copyResized` picks the nearest source pixel for each destination pixel. `copyResampled` averages the source pixels that land on each destination pixel, which is the supersampling downscale from the [algorithms chapter](raster-algorithms). The example shrinks the `primitives.png` sheet saved earlier both ways: ```perl use strict; use warnings; use GD; my $src = GD::Image->newFromPng('primitives.png', 1); my ($sw, $sh) = $src->getBounds; # fit inside 160x160, keeping the aspect ratio my $scale = 160 / ($sw > $sh ? $sw : $sh); my ($dw, $dh) = (int($sw * $scale + 0.5), int($sh * $scale + 0.5)); my $fast = GD::Image->new($dw, $dh, 1); my $smooth = GD::Image->new($dw, $dh, 1); # src dstX dstY srcX srcY dstW dstH srcW srcH $fast->copyResized($src, 0, 0, 0, 0, $dw, $dh, $sw, $sh); $smooth->copyResampled($src, 0, 0, 0, 0, $dw, $dh, $sw, $sh); for ([nearest => $fast], [resampled => $smooth]) { my ($name, $im) = @$_; my %colours; for my $y (0 .. $dh - 1) { $colours{ $im->getPixel($_, $y) }++ for 0 .. $dw - 1; } printf "%-9s %dx%d, %d distinct colours\n", $name, $dw, $dh, scalar keys %colours; open my $fh, '>', "thumb-$name.png" or die "thumb-$name.png: $!"; binmode $fh; print $fh $im->png; close $fh; } ``` ```text nearest 160x120, 6 distinct colours resampled 160x120, 133 distinct colours ``` The nearest-neighbour thumbnail keeps only the six colours it was drawn with, and pays for it: one-pixel lines fall between the sampled pixels and vanish, so the rectangle outline and the arc from the primitives sheet are simply gone. The resampled thumbnail blends them into faint but present lines, which is where its 133 in-between colours come from. Use `copyResampled` for anything a person will look at, and `copyResized` for pixel art you want to stay blocky when enlarged. The same copy-a-rectangle shape does compositing: ```perl use strict; use warnings; use GD; my $canvas = GD::Image->new(100, 100, 1); my $blue = $canvas->colorAllocate(0, 0, 255); $canvas->filledRectangle(0, 0, 99, 99, $blue); my $patch = GD::Image->new(40, 40, 1); my $red = $patch->colorAllocate(200, 40, 40); $patch->filledRectangle(0, 0, 39, 39, $red); # src dstX dstY srcX srcY w h $canvas->copy($patch, 10, 10, 0, 0, 40, 40); # stamp it $canvas->copyMerge($patch, 50, 50, 0, 0, 40, 40, 50); # 50% blend my $backup = $canvas->clone; # an independent copy printf "copy: %s\n", join ' ', $canvas->rgb($canvas->getPixel(30, 30)); printf "copyMerge: %s\n", join ' ', $canvas->rgb($canvas->getPixel(70, 70)); printf "clone: %s\n", join ' ', $backup->rgb($backup->getPixel(70, 70)); ``` ```text copy: 200 40 40 copyMerge: 100 20 147 clone: 100 20 147 ``` `copy` stamps the source unchanged. `copyMerge` takes a last argument from 0 to 100 and mixes that percentage of the source into the destination: half of `200 40 40` plus half of `0 0 255` is `100 20 147`. `clone` returns a new image with the same pixels, for when you want to keep the original before filtering it. Storable's `dclone` is no substitute: `GD`'s Storable hooks hand it an empty object with no image behind it, and the first method called on that object dies. ```{index} single: GD; rotating single: GD; flipping single: GD; cropping ``` ## Rotating, flipping and cropping The quarter turns and mirror images are exact pixel moves, with no resampling and nothing lost. The `copy` forms return a new image and leave the source alone; `flipHorizontal` and `flipVertical` also come in place. `crop` returns a new image holding one rectangle of the source, given as an array reference of x, y, width and height. The example marks two corners of a card so the output can say where they went: ```perl use strict; use warnings; use GD; # a card that shows its orientation: red block top-left, blue block top-right my $card = GD::Image->new(60, 40, 1); my $white = $card->colorAllocate(255, 255, 255); my $red = $card->colorAllocate(200, 40, 40); my $blue = $card->colorAllocate(40, 90, 220); $card->filledRectangle(0, 0, 59, 39, $white); $card->filledRectangle(0, 0, 14, 14, $red); $card->filledRectangle(50, 0, 59, 9, $blue); # which corner pixel holds which block sub corners { my ($im) = @_; my ($w, $h) = $im->getBounds; my %at; for ([ 'top-left', 0, 0 ], [ 'top-right', $w - 1, 0 ], [ 'bottom-left', 0, $h - 1 ], [ 'bottom-right', $w - 1, $h - 1 ]) { my ($name, $x, $y) = @$_; my $c = $im->getPixel($x, $y); $at{red} = $name if $c == $red; $at{blue} = $name if $c == $blue; } return sprintf "%dx%d red %-12s blue %s", $w, $h, $at{red} // '-', $at{blue} // '-'; } print "original ", corners($card), "\n"; for my $method (qw(copyRotate90 copyRotate180 copyRotate270 copyFlipHorizontal copyFlipVertical)) { my $turned = $card->$method; # a new image printf "%-19s %s\n", $method, corners($turned); } my $mirror = $card->clone; $mirror->flipHorizontal; # in place print "flipHorizontal ", corners($mirror), "\n"; my $piece = $card->crop([0, 0, 30, 20]); # [x, y, width, height] print "crop ", corners($piece), "\n"; # trim a border: every edge row and column within 0.5% of white goes my $trimmed = $card->cropThreshold($white, 0.5); print "cropThreshold ", corners($trimmed), "\n"; # a contact sheet of the card and its three rotations my $sheet = GD::Image->new(250, 70, 1); $sheet->filledRectangle(0, 0, 249, 69, $sheet->colorAllocate(200, 200, 200)); my $x = 5; for my $im ($card, $card->copyRotate90, $card->copyRotate180, $card->copyRotate270) { my ($w, $h) = $im->getBounds; $sheet->copy($im, $x, 5, 0, 0, $w, $h); $x += $w + 5; } open my $fh, '>', 'rotations.png' or die "rotations.png: $!"; binmode $fh; print $fh $sheet->png; close $fh; ``` ```text original 60x40 red top-left blue top-right copyRotate90 40x60 red top-right blue bottom-right copyRotate180 60x40 red bottom-right blue bottom-left copyRotate270 40x60 red bottom-left blue top-left copyFlipHorizontal 60x40 red top-right blue top-left copyFlipVertical 60x40 red bottom-left blue bottom-right flipHorizontal 60x40 red top-right blue top-left crop 30x20 red top-left blue - cropThreshold 60x15 red bottom-left blue top-right ``` `copyRotate90` turns the image a quarter **clockwise** as it appears on screen, the red corner moving from top-left to top-right, and swaps width and height; `copyRotate270` is the quarter turn the other way. The contact sheet `rotations.png` shows the card and its three turns side by side. A flip is a mirror, not a turn: after `copyFlipHorizontal` the blue block is on the left, but both blocks are still at the top, which no rotation achieves. `cropThreshold` strips a uniform border instead of taking a rectangle you name. It removes rows and columns from each edge for as long as every pixel in them lies within the threshold of the colour you pass, the threshold being a percentage of the largest possible colour distance, measured squared. On the card only the bottom goes: the top row, the left column and the right column each touch a coloured block, so the white below the red block's 15 rows is all it can take. A threshold of `0` matches nothing, so a small positive value is the usual choice even for an exact colour. ```{index} single: GD; filters single: convolution; in GD ``` ## Filters libgd ships a set of whole-image filters. Each works in place and returns true on success. Most of them are fixed 3 x 3 [convolutions](raster-algorithms), the operation the algorithms chapter wrote by hand, which the example below makes visible by reading one pixel on the edge of a red square and one inside it: ```perl use strict; use warnings; use GD; # a test card: a red square on white, so edges show what a filter does sub card { my $im = GD::Image->new(40, 40, 1); my $white = $im->colorAllocate(255, 255, 255); my $red = $im->colorAllocate(200, 40, 40); $im->filledRectangle(0, 0, 39, 39, $white); $im->filledRectangle(10, 10, 29, 29, $red); return $im; } my @filters = ( [ gaussianBlur => [] ], [ meanRemoval => [] ], # sharpen [ emboss => [] ], [ edgeDetectQuick => [] ], [ negate => [] ], [ grayscale => [] ], [ brightness => [40] ], # -255 .. 255 [ contrast => [50] ], # positive lowers contrast, negative raises it [ smooth => [1] ], # weight of the centre pixel [ pixelate => [8, 0] ], # block size, mode ); for my $f (@filters) { my ($method, $args) = @$f; my $im = card(); $im->$method(@$args) or die "$method failed\n"; printf "%-16s edge=%-12s inside=%s\n", $method, join('/', $im->rgb($im->getPixel(10, 20))), join('/', $im->rgb($im->getPixel(20, 20))); } ``` ```text gaussianBlur edge=213/93/93 inside=200/40/40 meanRemoval edge=35/0/0 inside=200/40/40 emboss edge=209/255/255 inside=127/127/127 edgeDetectQuick edge=17/0/0 inside=127/127/127 negate edge=55/215/215 inside=55/215/215 grayscale edge=87/87/87 inside=87/87/87 brightness edge=240/80/80 inside=240/80/80 contrast edge=145/105/105 inside=145/105/105 smooth edge=218/111/111 inside=200/40/40 pixelate edge=255/255/255 inside=200/40/40 ``` Reading the table: - **`gaussianBlur`** changes only the edge. Its kernel is the 3 x 3 `1 2 1 / 2 4 2 / 1 2 1`, divided by 16; at the edge pixel the left column (weight 4 of 16) is white, so red becomes `(4 x 255 + 12 x 200) / 16 = 213`. One pass is a small blur; call it several times for more. - **`meanRemoval`** is the sharpen kernel: nine times the centre minus the eight neighbours. Flat areas are untouched and the edge is pushed away from its neighbours, down to `35/0/0`. - **`emboss`** and **`edgeDetectQuick`** respond only to change, and add a mid-grey offset of 127 so that "no change" is grey rather than black. That is why the flat inside reads `127/127/127`. - **`negate`**, **`grayscale`** and **`brightness`** are per-pixel, not convolutions, so edge and inside change alike. `grayscale` uses the luma weights (`0.299 R + 0.587 G + 0.114 B`), giving 87 for this red. - **`contrast`** is per-pixel too, and runs the way its name does not suggest: a positive value **lowers** the contrast. Every channel's distance from mid-grey (127.5) is scaled by `((100 - c) / 100)` squared, so `50` keeps a quarter of it and red's `200` becomes `127.5 + 72.5 / 4 = 145`. A negative value pushes channels apart instead, and `-50` already drives this red to pure `255/0/0`. - **`smooth`** is a convolution with a centre weight you choose and weight 1 on the eight neighbours. With `1` all nine are equal, the plain box blur: three white pixels of nine give `(3 x 255 + 6 x 200) / 9 = 218`. A larger weight keeps more of the original pixel, so the blur gets weaker. Both `smooth` and `contrast` take fractional values, such as `smooth(0.5)` or `contrast(12.5)`. - **`pixelate`** replaces each block with one colour; mode `0` takes the block's top-left pixel, mode `1` its average. The edge pixel sits in a block whose top-left corner is white. Also available is `selectiveBlur`, which smooths regions while leaving strong edges alone (on the test card it leaves the edge exactly as it was). ```{index} single: GD::Simple single: turtle graphics ``` ## GD::Simple `GD::Simple` ships with `GD` and wraps an image in a friendlier API: named colours (190 of them), a pen with a current position, and separate fill (`bgcolor`) and outline (`fgcolor`) colours, so one call draws a filled shape with a border. ```perl use strict; use warnings; use GD::Simple; my $img = GD::Simple->new(220, 120); # cleared to white $img->bgcolor('yellow'); # fill colour $img->fgcolor('blue'); # outline colour $img->rectangle(10, 10, 90, 60); $img->bgcolor('orange'); $img->fgcolor('red'); $img->moveTo(150, 40); # shapes draw at the pen position $img->ellipse(80, 50); $img->fgcolor('black'); $img->penSize(3, 3); $img->moveTo(10, 100); $img->angle(0); # heading: 0 points right $img->line(100); # turtle-style: 100 px ahead $img->moveTo(130, 105); $img->font(gdMediumBoldFont); $img->string('GD::Simple'); open my $fh, '>', 'simple.png' or die "simple.png: $!"; binmode $fh; print $fh $img->png; close $fh; printf "%d colour names\n", scalar(my @names = $img->color_names); ``` ```text 190 colour names ``` The result is a yellow box with a blue border, an orange ellipse with a red border, a thick black line and a caption. Any method `GD::Simple` does not define is passed through to the `GD::Image` inside it, so the filters, `copyResampled` and the rest of this chapter all work on a `GD::Simple` object, and `$img->gd` returns the image itself. It is a good fit for charts, diagrams and quick sketches; for anything that needs exact control of colours and alpha, the plain `GD::Image` API is more direct. ## A complete program Putting it together: a poster with a gradient background, two translucent discs whose overlap blends, a blur pass, an anti-aliased line and a TrueType caption drawn crisp on top, saved as a PNG plus a resampled JPEG thumbnail. ```perl use strict; use warnings; use GD; my ($W, $H) = (400, 300); my $img = GD::Image->new($W, $H, 1); # background: a vertical ramp from dark blue to near black for my $y (0 .. $H - 1) { my $t = $y / ($H - 1); my $c = $img->colorAllocate(int(30 * (1 - $t)), int(40 * (1 - $t)), int(90 - 70 * $t)); $img->line(0, $y, $W - 1, $y, $c); } # two translucent discs; alpha blending composites the overlap my $sun = $img->colorAllocateAlpha(255, 200, 60, 40); my $sea = $img->colorAllocateAlpha(60, 200, 255, 40); $img->filledEllipse(150, 140, 160, 160, $sun); $img->filledEllipse(250, 140, 160, 160, $sea); # soften: each gaussianBlur pass is one 3x3 kernel, so repeat for more $img->gaussianBlur for 1 .. 3; # a crisp anti-aliased horizon and a caption, drawn after the blur $img->setAntiAliased($img->colorAllocate(255, 255, 255)); $img->line(20, 250, 380, 244, gdAntiAliased); my $font = '/usr/share/fonts/TTF/DejaVuSans.ttf'; my $ink = $img->colorAllocate(240, 240, 240); $img->stringFT($ink, $font, 16, 0, 20, 285, 'Perl, drawn with GD') or die "stringFT: $@"; sub save { my ($file, $bytes) = @_; open my $fh, '>', $file or die "$file: $!"; binmode $fh; print $fh $bytes; close $fh; } save('poster.png', $img->png); # a half-size thumbnail my $thumb = GD::Image->new($W / 2, $H / 2, 1); $thumb->copyResampled($img, 0, 0, 0, 0, $W / 2, $H / 2, $W, $H); save('poster-thumb.jpg', $thumb->jpeg(85)); ``` Note that the background allocates 300 colours in a loop. On a true-colour image that costs nothing, because `colorAllocate` only packs the channels; on a palette image the table would be full after 256 lines. Order matters too: the blur runs before the line and the caption, so the discs are soft and the text is sharp. ## When GD is the wrong tool `GD` is raster, so it has the raster limitation: it works at a fixed resolution, and scaling past it degrades. Its anti-aliasing covers lines and polygons only, and its coordinates are integers. For smooth curves, sub-pixel positioning, gradients and output that must stay crisp at any size (a logo, a diagram destined for print or the web), use a vector engine: [Cairo and SVG](vector-cairo-svg) covers both routes, and pperl's native `Cairo` binding is shown in [the Cairo showcase](ffi-cairo-showcase). When the job is a per-pixel algorithm no filter covers (a custom kernel, a colour transform, a generated texture), write it over the pixels yourself. For small images the plain-Perl loops of the [algorithms chapter](raster-algorithms) are enough, and `setPixel` and `getPixel` move the result in and out of `GD`; for large images, `PDL` does the same arithmetic on whole arrays at once. When the drawing needs to appear in a live window rather than a file, that is the [GUI toolkits chapter](gui-toolkits). For everything that is "make or modify a bitmap on disk", `GD` is a small, fast and complete answer.