Pop PHP
The Toolkit

Images

pop-image puts one API in front of PHP's two image extensions. Pick an adapter — GD or Imagick — and everything after that is the same calls: resize, crop, rotate, adjust, filter, draw, save.

BASH
composer require popphp/pop-image

Loading an Image#

Pop\Image\Image is a factory with one method per adapter and source. Loading from disk gives you the adapter object, and every editing method returns it back for chaining:

PHP
use Pop\Image\Image;

$image = Image::loadGd(__DIR__ . '/../public/uploads/photo.jpg');

$image->getWidth();   // 400
$image->getHeight();  // 300
$image->getFormat();  // 'jpg'

Image::loadImagick() is the same call against the other backend. loadGdFromString() and loadImagickFromString() take raw bytes and a filename, for an image that arrived over HTTP or came out of a database. createGd() and createImagick() make a blank truecolor image at a given size, and createGdIndex()/createImagickIndex() make a palette-based one for GIF:

PHP
use Pop\Image\Image;

$image = Image::createGd(400, 300, 'banner.jpg');

Beyond the dimensions and format, a loaded image reports getName(), getQuality(), getColorspace(), isGray(), isRgb(), isCmyk(), isIndexed(), and getExif() — EXIF data is read automatically from JPEGs on load. destroy() frees the image from memory, and destroy(true) also deletes the file from disk, which matters when a script walks a large directory.

Image::loadGd() and Image::loadImagick() throw Pop\Image\Adapter\Exception for a path that does not exist, so a bad path fails at the call.

Resize, Scale and Crop#

Six methods cover the geometry, and all of them preserve aspect ratio except crop(). Against a 400 by 300 source:

Call Result
resize(100) 100x75 — constrains the larger dimension
resizeToWidth(100) 100x75 — constrains width
resizeToHeight(100) 133x100 — constrains height
scale(0.5) 200x150 — multiplies both dimensions
crop(120, 80, 100, 200) 120x80 — width, height, then x and y offset
cropThumb(100) 100x100 — a centered square
PHP
use Pop\Image\Image;

Image::loadGd(__DIR__ . '/../public/uploads/photo.jpg')
    ->resize(100)
    ->setQuality(60)
    ->writeToFile(__DIR__ . '/../public/uploads/photo-thumb.jpg');

crop()'s last two arguments are the offset of the crop window within the source, defaulting to the top left. cropThumb() centers itself unless you pass an offset as its second argument, which makes it the one to reach for when generating avatars and thumbnails from arbitrary uploads.

rotate() turns the image clockwise by a number of degrees and takes an optional Pop\Color\ColorInterface for the background exposed by the rotation — the GD adapter takes a further alpha argument from 0 to 127. flip() mirrors over the x-axis, flop() over the y-axis:

PHP
use Pop\Color\Color\Rgb;
use Pop\Image\Image;

Image::loadGd(__DIR__ . '/../public/uploads/photo.jpg')
    ->rotate(90, new Rgb(255, 255, 255))
    ->flop()
    ->writeToFile(__DIR__ . '/../public/uploads/photo-rotated.jpg');

Rotating by 90 swaps the dimensions, so a 400 by 300 image comes out 300 by 400.

Quality is a separate setting from geometry. setQuality() takes 0 to 100 and applies at write time, and writeToFile() and outputToHttp() both take it as an argument instead. A value outside that range throws \OutOfRangeException ("Error: The quality parameter must be between 0 and 100"), and so does a GD alpha outside 0 to 127.

Adjust and Filter#

Everything beyond geometry lives on six editing objects, reached from the image by the method of the same name — adjust(), filter(), draw(), effect(), layer() and type(). You never construct one; the adapter returns the object matched to its own backend. A property read, $image->adjust, is an alias for the same call.

adjust() makes tonal changes. Both backends have brightness(), contrast() and desaturate(); Imagick adds hue(), saturation(), hsb() and level():

PHP
use Pop\Image\Image;

$image = Image::loadGd(__DIR__ . '/../public/uploads/photo.jpg');

$image->adjust()->brightness(20)
                ->contrast(10)
                ->desaturate();

$image->writeToFile(__DIR__ . '/../public/uploads/photo-flat.jpg');

filter() is where the two backends diverge most. GD wraps a handful of PHP's built-in imagefilter() operations — blur(), sharpen(), negate(), colorize(), pixelate() and pencil(). Imagick exposes far more of ImageMagick: adaptiveBlur(), gaussianBlur(), motionBlur(), paint(), posterize(), noise(), diffuse(), skew(), swirl() and wave(), and its shared methods take radius and sigma rather than a single amount.

PHP
use Pop\Image\Image;

$image = Image::loadImagick(__DIR__ . '/../public/uploads/photo.jpg');

$image->filter()->swirl(30);
$image->adjust()->hue(30);

$image->writeToFile(__DIR__ . '/../public/uploads/photo-swirled.jpg');

effect() fills the whole canvas: fill(), border(), radialGradient(), verticalGradient(), horizontalGradient() and linearGradient(). layer()->overlay($file, $x, $y) composites another image on top, and Imagick adds flatten() for a multi-frame image. Every color argument is a Pop\Color\ColorInterface, so Rgb, Hex, Cmyk, Grayscale and Hsl all work.

Check which adapter you hold before calling an Imagick-only method — the method is simply absent from the GD class.

Drawing#

draw() puts shapes on the image. Four setters hold the state — setFillColor(), setStrokeColor(), setStrokeWidth() and setOpacity() — and every shape afterward uses whatever is currently set:

PHP
use Pop\Color\Color\Rgb;
use Pop\Image\Image;

$image = Image::createGd(400, 200, 'card.png');

$image->effect()->fill(new Rgb(245, 245, 245));

$image->draw()->setFillColor(new Rgb(0, 120, 215))
              ->setStrokeColor(new Rgb(0, 0, 0))
              ->setStrokeWidth(2)
              ->rectangle(20, 20, 160, 80);

$image->draw()->setFillColor(new Rgb(220, 50, 50))
              ->circle(300, 60, 40);

$image->writeToFile(__DIR__ . '/../public/img/card.png');

Both backends have line(), rectangle(), square(), ellipse(), circle(), arc(), chord(), pie() and polygon(). Imagick adds roundedRectangle() and roundedSquare(). Where a shape takes a width and a height, omitting the height makes it equal to the width — ellipse(200, 100, 60) is a circle. GD accepts floats for coordinates and sizes; Imagick takes integers only.

polygon() takes a list of point maps rather than a flat coordinate array: [['x' => 40, 'y' => 50], ['x' => 100, 'y' => 120]].

type() draws text, with the same fill, stroke and opacity setters plus font(), size(), x(), y(), xy() and rotate(). text() renders the string using whatever is set at that moment:

PHP
use Pop\Color\Color\Rgb;
use Pop\Image\Image;

$image = Image::createGd(300, 100, 'label.png');

$image->effect()->fill(new Rgb(255, 255, 255));

$image->type()->font('/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf')
              ->size(24)
              ->setFillColor(new Rgb(0, 0, 0))
              ->xy(20, 60)
              ->text('Hello World');

$image->writeToFile(__DIR__ . '/../public/img/label.png');

font() wants a path to a TrueType file. Without one, GD falls back to a built-in bitmap font where size() means an index from 1 to 5 rather than a point size.

Give the text methods a font path that exists — GD emits a PHP warning and draws nothing otherwise.

GD vs Imagick#

Both extensions were loaded on the machine these samples were run on, so everything on this page is executed output from both backends. GD 2.3.3 and the imagick extension are the two the component supports, and pop-image requires GD outright — Imagick is optional.

Ask before you assume. Image::getAvailableAdapters() returns a map and Image::isAvailable() takes one name:

PHP
use Pop\Image\Image;

Image::getAvailableAdapters();   // ['gd' => true, 'imagick' => true]

Image::isAvailable('imagick');   // true
Image::isAvailable('bogus');     // false

Both are checked cheaply — function_exists('gd_info') and class_exists('Imagick') — and isAvailable() accepts imagemagick as a synonym for imagick. An unknown name returns false rather than throwing, so a typo reads as "not available" and silently sends you down the GD path.

The differences that decide which one you want:

GD Imagick
Formats JPG, PNG and GIF only whatever ImageMagick was built with — TIFF, WebP, BMP, PDF and more
getFormat() on a JPEG 'jpg' 'jpeg'
Adjust brightness, contrast, desaturate those plus hue, saturation, hsb, level
Filters blur, sharpen, negate, colorize, pixelate, pencil a much larger set, and radius/sigma arguments instead of one amount
Shapes everything but rounded corners adds roundedRectangle() and roundedSquare()
Layers overlay() overlay(), plus flatten() and multi-frame handling
Coordinates int or float int only

convert() is where the format limit bites. On the Imagick adapter it retargets the image to any format the extension supports; on the GD adapter, anything outside its three throws Pop\Image\Adapter\Exception ("Error: The image type must be a GIF, PNG or JPG"):

PHP
use Pop\Image\Image;

Image::loadImagick(__DIR__ . '/../public/uploads/photo.jpg')
    ->convert('tiff')
    ->writeToFile(__DIR__ . '/../public/uploads/photo.tif');

Loading has the same limit and reports it the same way: (new Pop\Image\Adapter\Gd())->load($tiff) throws Pop\Image\Adapter\Exception ("Error: The image file must be a GIF, PNG or JPG"). Note that the check is on the filename's extension, not on the file's contents.

Take GD for thumbnails and simple crops of web formats, which is most image work and needs no extra extension. Take Imagick when the format list, the filter set or the layer handling is what you are there for.

Saving and Converting#

Two outputs. writeToFile() saves to disk, taking a path and an optional quality, and outputToHttp() streams the image to the client:

PHP
use Pop\Image\Image;

$image = Image::loadGd(__DIR__ . '/../public/uploads/photo.jpg');
$image->resize(800);

$image->writeToFile(__DIR__ . '/../public/uploads/photo-large.jpg', 85);

outputToHttp(?int $quality, ?string $to, bool $download, bool $sendHeaders, array $headers) sends the right Content-Type for the image's format and a Content-Disposition chosen by $downloadinline when it is false, attachment when true, using $to as the filename. Pass false for $sendHeaders when the response layer is sending its own, and extra headers as the last argument.

Neither output method changes the image's format. That's convert(), which retargets the image and returns it for chaining, so the write that follows produces the new format:

PHP
use Pop\Image\Image;

Image::loadGd(__DIR__ . '/../public/uploads/photo.jpg')
    ->convert('png')
    ->writeToFile(__DIR__ . '/../public/uploads/photo.png');

Match the filename extension to what you converted to. Nothing cross-checks them, and load() dispatches on the extension rather than the contents, so a PNG written as photo.jpg fails to load again.

destroy() releases the image resource. In a script converting a directory of files, calling it between images is what keeps the process inside its memory limit; destroy(true) also unlinks the file it was loaded from.

The full method set on both adapters, the Imagick-specific resolution, compression and blur settings, and every filter argument go past what this page covers — see the pop-image README.

See Also#

  • File Storage — where the uploads these samples read come from and go back to
  • CSS & Color — the Pop\Color classes every fill, stroke and gradient takes
  • PDFs — placing an image into a document
  • Requests & Responses — the response outputToHttp() writes into
  • pop-image README — every adapter method and the full editing-object tables