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.
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:
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:
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 |
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:
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():
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.
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:
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:
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:
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"):
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:
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 $download —
inline 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:
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\Colorclasses 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