belisoful / prado-image
Image containers, metadata editing, and codecs for the PRADO PHP framework: read-write JPEG, TIFF, PNG, WebP/RIFF, and GIF87a/89a; EXIF through Exif 3.1 with makernotes, XMP (ISO 16684-1), IPTC (IIM), Photoshop 8BIM, JFIF/JFXX, JUMBF, PrintIM, and Exif WAVE audio; read-write ICC profiles with a pure
Package info
github.com/belisoful/prado-image
Type:prado4-extension
pkg:composer/belisoful/prado-image
Requires
- php: >=8.1.0
Requires (Dev)
- friendsofphp/php-cs-fixer: ^3.94
- phpdocumentor/shim: ^3
- phpstan/phpstan: ^2.1
- phpunit/phpunit: ^10
- pradosoft/prado: ^4.4@dev
Suggests
- ext-dom: The DOM behind TXMP; required for reading or writing XMP packets.
- ext-gd: Raster conversion as \GdImage objects (TImageGraphics's preferred library).
- ext-iconv: Charset conversion of IPTC text, EXIF UserComment, and Photoshop unicode strings.
- ext-imagick: Raster conversion as \Imagick objects (TImageGraphics's alternate library); required for lookup-table ICC transforms, embedded profiles, and high bit depths.
- ext-zlib: Deflate for PNG iCCP/zTXt chunk payloads.
Provides
None
Conflicts
None
Replaces
None
README
Read and write the metadata inside image files — EXIF, XMP, IPTC, ICC profiles and the rest — from PHP, without re-encoding the picture. An extension for the PRADO PHP Framework 4.4+.
If you have ever needed to strip the GPS coordinates out of a photo before publishing it, add a caption to a JPEG without putting it through another lossy generation, or find out which lens took a shot, that is what this is for.
The central promise is that editing metadata leaves your pixels alone. These readers parse the
image container and rewrite only the parts you changed; the compressed image data comes
through byte-identical. When you do want to change the pixels, getImage() / setImage() /
fromImage() are there, and they carry the metadata across for you.
Formats
Every format here is supported in both directions — reading and writing.
- JPEG — parsed segment by segment: JFIF/JFXX, EXIF, XMP, Kodak Meta, Picture Info, Photoshop IRB/IPTC, ICC, and the comment. Edit any of them and the entropy-coded scan is copied through untouched.
- TIFF — a complete TIFF 6.0 engine, both byte orders and all twelve data types. Strip and tile data is relocated with recomputed offsets, so metadata edits are lossless. The raster decodes too: uncompressed, PackBits, LZW and the CCITT fax codings, at 1–16 bits, striped or tiled, across bilevel, grayscale, palette, RGB, CMYK, YCbCr and L*a*b*.
- PNG — chunk-level, with the normative chunk order maintained and CRCs recomputed. Reads
and writes every carrier PNG defines, plus the
Raw profile type 8bimtext chunk that Photoshop, ImageMagick and ExifTool use to exchange IPTC (PNG has no IPTC chunk of its own). APNG animation is first class — frames round-trip byte-faithfully. - WebP / RIFF — dimensions from
VP8,VP8LorVP8X; ICC, EXIF and XMP read and written, with theVP8Xheader and its feature flags kept in step automatically. - GIF 87a and 89a — the whole standard at block level, animation included. Frames are kept exactly as authored rather than coalesced, so sub-rectangles, disposal, interlacing and local palettes survive a round trip unchanged.
Two of those formats simply have nowhere to put IPTC — WebP and GIF — so setIPTC() throws
rather than accepting records it would quietly lose.
Metadata
- EXIF — the full model over IFD0/EXIF/GPS/Interoperability/IFD1, with all 220 known tags of Exif 3.1 (CIPA DC-008-2026) and human-readable interpretation of their values. Rewriting pins the maker note at its original byte offset, because maker notes are full of pointers into themselves and moving one corrupts it.
- Maker notes — thirteen manufacturers decoded (Agfa, Canon, Casio, Epson, Fujifilm, Konica/Minolta, Kyocera/Contax, Nikon, Olympus, Panasonic, Pentax, Ricoh, Sony), quirks and all: forced byte orders, note-relative offsets, embedded TIFFs, packed camera settings.
- XMP — a DOM-backed model of the entire ISO 16684-1 grammar: arrays, nested structures,
qualifiers and language alternatives. It knows the standard schemas, so writing
dc:titlegives you a language alternative anddc:subjectaBagwithout your having to say so. Extended XMP in JPEG is split and rejoined transparently. - IPTC (IIM 4.1) — the full record set as a map, addressable by tag name or
record#dataset. - Photoshop IRB — every 8BIM resource, with typed decoders and the embedded IPTC bridge.
- ICC profiles — not just swapped whole but genuinely edited: every header field, typed tag
reads and writes, and the profile-id digest.
TICCTransformconverts pixels between matrix/TRC spaces in pure PHP, which gives GD colour management it has no API for. - And the smaller ones: JFIF/JFXX thumbnails, JUMBF boxes, PrintIM, APP12 Picture Info, the Photoshop File Info 22-field sync layer, and the WAVE audio files cameras record beside photographs.
Two things worth knowing about
Privacy scrubbing. clearPrivateData() removes identifying information — location,
authorship, camera identity, serial numbers, timestamps, software, maker notes, thumbnails —
from every carrier a file holds, in a single call, by category. It leaves everything that
describes the picture itself, so what you get back is still a useful photo.
Private spaces. The maker notes an EXIF rewrite must not disturb are exposed as byte ranges and bridged to the framework's reserved-space streams, so a consumer can edit around them with the same protection the writer itself applies.
Streams
Everything works with PSR-7 streams and PHP stream resources as well as strings and files —
fromStream() and writeTo() throughout, partial writes honoured.
$jpeg = TJPEG::fromStream(new TBinaryStream(TStream::fromFile('in.jpg'))); $jpeg->getEXIF()?->setValueByName('Artist', 'A. Photographer'); $jpeg->writeTo(fopen('out.jpg', 'wb')); // resource out $exif = TEXIF::fromStream(new TLimitStream($file, $len, $off)); // windowed parse
Very large TIFFs get a lazy scan: TEXIF::scanFile() seeks through the header, IFD chains and
thumbnail without ever reading the pixel data, so the metadata of a multi-gigabyte scan loads
in kilobytes.
$exif = TEXIF::scanFile('500MB-scan.tif'); // seeks; never loads the pixels $exif->getMake(); $exif->getThumbnail();
Requirements
| Requirement | Scope | Purpose |
|---|---|---|
| PHP 8.1 or higher | required | The only hard requirement |
PRADO Framework ^4.4 |
dev | TComponent/TMap, the TStream IO layer (TLimitStream, TResourceType, the stream filters), and Prado\IO\Compression\ICompressor |
ext-gd |
suggested | Thumbnail/caption conversion as \GdImage (the preferred TImageGraphics library); generates the unit-test fixtures |
ext-imagick |
suggested | Thumbnail/caption conversion as \Imagick (the alternate TImageGraphics library) |
ext-iconv |
suggested | IPTC charset conversion via the framework's TEscCharsetConverter |
Installation
composer require belisoful/prado-image
What it provides
| Class | Role |
|---|---|
TImageFile |
The abstract reader base: fromString()/fromStream()/fromFile() factories, getWidth()/getHeight()/getFormat(), IPTC and ICC profile accessors, and toBinary()/toStream()/save() composition |
TJPEG |
The JFIF/EXIF JPEG reader/writer: keeps every segment in order, parses JFIF/JFXX, Photoshop IPTC, and the ICC profile into editable objects, preserves the scan verbatim on metadata edits and swaps it on setImage(); protected hooks let a subclass ingest additional markers (e.g. APP1 EXIF/XMP) |
TPNG |
The PNG chunk reader/writer: IHDR dimensions, order-maintaining chunk mutators, read-write iCCP ICC / eXIf EXIF / iTXt XMP / 8BIM IRB (and its IPTC), raster getImage()/setImage(), and animated PNG (getApngFrames()/setApngFrames()/fromApngImages()) |
TAPNGFrame |
One authored APNG frame: geometry (size, canvas offset), delay, disposal and blend operations, and the frame image data |
TWebP |
The WebP reader/writer: a RIFF container with dimensions from VP8/VP8L/VP8X, read-write ICCP/EXIF/XMP chunks with their VP8X flags, and raster getImage()/setImage() |
TRIFF |
The generic RIFF container walker (WAV, AVI, WebP): form type plus chunk id/size/offset/payload, with setChunk()/addChunk()/prependChunk()/insertChunk()/setChunkInOrder()/removeChunk() |
TGIF |
The GIF87a/GIF89a reader/writer: logical screen descriptor, global color table, the ordered frame and extension stream, loop count, comments, read-write XMP (XMP DataXMP) and ICC (ICCRGBG1012), raster getImage()/setImage(), and byte-faithful composition |
TGIFFrame |
One authored GIF frame: sub-rectangle, interlace, local color table, LZW pixel indexes, and the graphic control fields (delay, disposal, user input, transparency) |
TGIFExtension |
One GIF extension block, with its sub-block framing and application identity preserved verbatim, and the raw-payload mode (magic trailer) the XMP packet needs |
TImageChunk |
One chunk of a chunked container (PNG or RIFF): type, size, offset, data |
TPhotoshop8BIM |
The Photoshop 8BIM image-resource codec wrapping IPTC in a JPEG APP13 segment: iptcDecode()/iptcEncode() |
TIPTC |
The IPTC IIM 4.1 record set as a TMap: array access by tag name or record#dataset id, per-dataset validation and coercion, parse()/toBinary(), the hasIPTC()/hasEXIF()/hasXMP()/hasICCProfile() common-metadata accessors (with get/set), and GD conversion of the 1-bit RasterizedCaption dataset |
TIPTCTags / TIPTCRecords |
The IIM dataset identifiers (Envelope, Application, NewsPhoto, object-data records) and record-number enumerations |
TJFIF |
The JPEG APP0 JFIF data: version, pixel density and units, and an optional RGB thumbnail (≤ 255x255) |
TJFXX |
The JPEG APP0 JFXX thumbnail extension: JPEG, palette, or RGB encodings, with GD/Imagick conversion both ways |
TJFIFFormat |
The JFIF/JFXX embedding-mode enumeration (including the pick-the-most-compact JFXXEfficiency) |
TTIFF |
The TIFF image file, read-write: dimensions, full EXIF metadata, IPTC, ICC profile, XMP (tag 700) and the Photoshop IRB, lossless metadata rewriting, raster decode/encode (getImage()/setImage()/fromImage()) across none/LZW/PackBits/CCITT compressions, and the private-space stream views (getReservedSpaces()/toReservedSpaceStream()/toFreeSpaceStream()) |
TCCITTFaxCompressor |
The CCITT bilevel fax codec, encode and decode: Modified Huffman, Group 3 (1D and 2D), Group 4 |
TTIFFRaster |
The TIFF raster decoder behind TTIFF::getImage(): strips and tiles, chunky and planar, 1/2/4/8/16-bit samples, either fill order, and the bilevel/grayscale/palette/RGB/CMYK/YCbCr/L*a*b* photometrics, normalized to 8-bit RGB |
TTIFFDocument / TTIFFIfd / TTIFFTag / TTIFFDataType |
The TIFF 6.0 engine: tolerant parsing (warnings, not failures), offset-recomputing composition with pinned value areas, and all twelve data types |
IPrivacyScrubbable |
The privacy contract: clearPrivateData(int $types = TPrivacyCategory::All): int on every metadata carrier and every image container |
TPrivacyCategory |
The identifying-information categories clearPrivateData() removes, one bit each (Location, Author, Description, CameraModel, SerialNumber, Timestamp, Software, MakerNote, Thumbnail, Interoperability) with the Identity/Provenance presets and All (-1); its docblock tabulates what each flag removes in each carrier |
TEXIF |
The EXIF model: named IFDs, tag access by name (getValueByName('FNumber')), interpreted text (getTextByName), the IFD1 thumbnail, the embedded IPTC/XMP/IRB/PIM/makernote bridges, the private-space stream views that protect the maker notes on write, and clearPrivateData() for privacy scrubbing by category |
TEXIFTags |
The EXIF-family tag knowledge base: TIFF/EXIF/GPS/Interoperability/Kodak groups with names, lookups, units, and special decoders |
TMakernote (+ TCanonMakernote, TKonicaMinoltaMakernote) |
The camera-makernote decoder, driven by the maker facts in TMakernoteTags::Headers, with a registerable per-maker class map |
TXMP |
The DOM-backed XMP packet: the full value grammar (arrays, nested and array-of structures, qualifiers, language alternatives), path expressions, enumeration, date/boolean helpers, the standard prefix registry, and xpacket serialization options |
TXMPSchemas |
The XMP schema registry: each standard property's declared form (LangAlt, Bag, Seq, Alt, Struct, Simple) across dc, xmp, xmpRights, xmpMM, xmpBJ, xmpTPg, xmpDM, photoshop, IPTC Core/Extension, PLUS, tiff, and exif — used to pick the right form on write and to validate() on read |
TPhotoshopIRB / TPhotoshopResource |
The Photoshop 8BIM resource set with per-resource typed decoders and APP13 chunking |
TFileInfo |
The Photoshop File Info emulation: the 22 synchronized fields across EXIF + XMP + IRB/IPTC |
TJUMBFBox / TJUMBFDescription |
The JUMBF box model (ISO/IEC 19566-5): superboxes, description boxes with the reserved content-type UUIDs, and the xml()/json()/exifAnnotation() builders; carried by TJPEG::getJumbfBoxes()/setJumbfBoxes() as APP11 |
TEXIFAudio |
The Exif WAVE audio file: the exif LIST attribute chunks, fmt /duration accessors, and lossless rewriting of the audio |
TPIM |
The Print Image Matching block codec (byte-order aware, Panasonic quirk handled) |
TPictureInfo |
The legacy APP12 picture-info text with vendor signatures and Key=Value field parsing |
TImageGraphics |
The graphics-library seam: routes RGB24 and CMYK export/import, decode, JPEG/PNG/WebP encode, ICC profile read/attach/transform, resample, black-and-white reduction, and palette quantization to the backend of the image's own library (or of the requested mode), with a settable default, a supports() capability query, and getCapableLibrary() |
TICCProfile |
The editable ICC profile: every header field readable and writable, the profile-id digest, the tag table with typed reads and writes (text/mluc localized text, XYZ , curv/para curves, sf32 arrays, colorant matrix, tone curves), tag removal and aliasing, and offset-recomputing composition |
TICCTransform |
The pure-PHP color transform between two matrix/TRC profiles, giving GD a color-managed conversion it has no API for |
IImageGraphicsLibrary |
The contract one graphics library implements: the raster operations, encode() (JPEG/PNG/WebP), the ICC profile pair, and supports() for the capability differences between backends |
TImageGraphicsGD / TImageGraphicsImagick |
The GD and ImageMagick implementations, each operating only on its own image type |
TImageGraphicsMode |
The graphics-library enumeration (GD, Imagick) |
TLZWCompressor / TGIFLZWCompressor |
TIFF-flavor and GIF-flavor LZW codecs, implementing the framework's Prado\IO\Compression\ICompressor |
TPackBitsCompressor |
The TIFF/Macintosh PackBits run-length codec |
THorizontalPredictor |
The TIFF horizontal-predictor transform that improves LZW/deflate ratios |
TLZWFilter / TPackBitsFilter / THorizontalPredictorFilter |
The streaming TStreamCodecFilter counterparts of the codecs |
The image_*/iptc_*/jfif_*/gif_* error codes and the Prado3 short class names are registered by Composer from composer.json extra.prado (config/errorMessages.txt, config/classMap.json).
Usage
Reading dimensions and metadata
use Prado\IO\Image\TJPEG; use Prado\IO\Image\TPNG; use Prado\IO\Image\TWebP; $jpeg = TJPEG::fromFile('photo.jpg'); // or fromString() / fromStream() [$w, $h] = [$jpeg->getWidth(), $jpeg->getHeight()]; $png = TPNG::fromFile('image.png'); $icc = $png->getICCProfile(); // null when absent $webp = TWebP::fromFile('image.webp'); // VP8 / VP8L / VP8X all supported
Editing IPTC and saving without re-encoding
use Prado\IO\Image\TJPEG; use Prado\IO\Image\Meta\TIPTC; use Prado\IO\Image\Meta\TIPTCTags; $jpeg = TJPEG::fromFile('photo.jpg'); $iptc = $jpeg->getIPTC() ?? new TIPTC(); $iptc[TIPTCTags::CaptionAbstract] = 'Edited caption'; $iptc['Keywords'] = ['prado', 'image', 'metadata']; // repeatable dataset $jpeg->setIPTC($iptc); $jpeg->save('photo-out.jpg'); // image data unchanged, metadata rewritten
JFIF density and thumbnails
$jfif = $jpeg->getJFIF(); [$jfif->getXDensity(), $jfif->getYDensity(), $jfif->getUnits()]; $jfif->setImage($gdThumbnail); // embed an RGB thumbnail (<= 255x255) // JFXX is the APP0 thumbnail extension, with JPEG, palette, or RGB encodings. $jfxx = $jpeg->getJFXX() ?? new TJFXX(); $jfxx->setImage($gdThumbnail, TJFXX::JPEG_THUMB); // or PALETTE_THUMB / COLOR_THUMB / EFFICIENCY_THUMB $thumb = $jfxx->getImage(); // back to a \GdImage or \Imagick $jpeg->setJFXX($jfxx);
EXIF: read, interpret, edit, rewrite
use Prado\IO\Image\TJPEG; $jpeg = TJPEG::fromFile('photo.jpg'); $exif = $jpeg->getEXIF(); $exif->getMake(); // 'Canon' $exif->getValueByName('FNumber'); // [[71, 10]] $exif->getTextByName('FNumber'); // '7.1' $exif->getTextByName('ExposureTime'); // '1/125 seconds' $exif->getTextByName('GPSLatitude'); // '34° 3' 30"' $thumb = $exif->getThumbnail(); // IFD1 JPEG bytes $note = $exif->getMakernote(); // detected via the maker registry $note?->getValues(); // name => interpreted text $note?->getThumbnail(); // Casio/Olympus/Minolta embedded thumbnail $exif->setValueByName('Artist', 'A. Photographer'); $jpeg->save('photo-out.jpg'); // rewritten; makernote pinned at its offset
GPS travels as decimal degrees, metres, and UTC instants — the helpers read and write
the reference-letter and degrees/minutes/seconds tag pairs (seeding the mandatory
GPSVersionID on first write):
$exif->getLatitude(); // -33.86882 (S negative, W negative) $exif->setLatitude(60.392990); $exif->setLongitude(5.324383); $exif->setAltitude(-42.25); // below sea level $exif->setGpsTimestamp(new DateTimeImmutable('now')); // stored as UTC
TIFF files read through the same model — TTIFF::fromFile('scan.tif')->getEXIF() — and the Kodak APP3 Meta block through $jpeg->getMeta().
Privacy: clearing identifying information
A photo leaving a user's control carries where, when, by whom, and with what it was
taken — and it says so in several places at once: EXIF, XMP, IPTC, the Photoshop IRB,
and format-specific fields such as the JPEG comment or PNG text chunks. Every carrier
and every container implements IPrivacyScrubbable, so clearPrivateData() removes
that by category — every category by default — while leaving the fields that describe
the picture (exposure, colour, dimensions), and the result is still a well-formed,
useful photo.
use Prado\IO\Image\TPrivacyCategory; // One call on the container reaches every metadata block it holds. $jpeg = TJPEG::fromFile('photo.jpg'); $removed = $jpeg->clearPrivateData(); // everything: the safe default $jpeg->save('photo-shareable.jpg'); $png->clearPrivateData(TPrivacyCategory::Location); // GPS in EXIF and XMP, IPTC place names $tiff->clearPrivateData(TPrivacyCategory::All & ~TPrivacyCategory::CameraModel); // keep make/model $webp->clearPrivateData(TPrivacyCategory::Identity | TPrivacyCategory::Location); // people + place $gif->clearPrivateData(TPrivacyCategory::Provenance); // where, when, and with what software // Or scrub a single carrier when that is what you hold. $exif = $jpeg->getEXIF(); $exif->clearPrivateData(TPrivacyCategory::MakerNote | TPrivacyCategory::Thumbnail); $jpeg->setEXIF($exif);
The categories are one bit each: Location (the GPS IFD, XMP GPS and place names, IPTC
city/state/country), Author (artist, copyright, credit, contact, xmpRights, IRB URLs),
Description (titles, captions, keywords, comments, JPEG/GIF comments, PNG text),
CameraModel, SerialNumber (body/lens serials, XMP document/instance ids, IPTC job and
document ids), Timestamp, Software, MakerNote (a proprietary blob that routinely
embeds serials and owner names), Thumbnail (a copy of the image that survives cropping
— a classic redaction leak: IFD1, XMP thumbnails, IRB and IPTC previews, JFIF/JFXX), and
Interoperability; Identity and Provenance are presets, and All is -1. The return
value is the number of items removed, and a second call removes nothing. IIM makes the
IPTC envelope's date and number mandatory, so the scrub replaces an existing envelope
date with a fixed synthetic sentinel (TIPTC::ScrubbedDate) and re-derives the number
from it, instead of letting the next write stamp today's date back in.
Private spaces: editing around the maker notes
A maker note carries pointers relative to its own position, so it must stay put when its EXIF is rewritten — the writer pins it. TEXIF and TTIFF expose those pinned ranges and hand the composed bytes to the framework's reserved-space stream decorators, so a consumer can edit the block while the maker notes are protected exactly as the writer protects them.
use Prado\IO\Stream\TReservedSpaceMode; $exif = $jpeg->getEXIF(); $exif->getReservedSpaces(); // [[offset, length], …] within toBinary() // A stream whose reserved ranges are untouchable. Skip writes through the free bytes // and jumps the maker notes; Clip stops at them; Fail throws on any overlap. $stream = $exif->toReservedSpaceStream(TReservedSpaceMode::Skip); $stream->seek(0); $stream->write($patch); // the maker notes come out byte-identical // Or see only the editable bytes as one contiguous stream — the maker notes are absent. $free = $exif->toFreeSpaceStream();
Only a parsed maker note has an on-disk offset to pin, so a freshly built one reserves nothing. TTIFF offers the same three methods for a TIFF file.
XMP
use Prado\IO\Image\Meta\TXMP; $xmp = $jpeg->getXMP() ?? TXMP::blank(); $xmp->getTitle(); // x-default $xmp->getTitle('de'); // language alternative $xmp->setLangAlt(TXMP::NS_DC, 'title', ['x-default' => 'Sunset', 'de' => 'Sonnenuntergang']); $xmp->setKeywords(['sunset', 'norway']); $xmp->getByPath('xmpMM:History[1]/stEvt:action'); // 'created' $xmp->addArrayItem(TXMP::NS_MM, 'History', ['stEvt:action' => 'edited'], 'Seq'); $xmp->getProperties(); // every property, by prefix:name $jpeg->setXMP($xmp); // extended XMP when > 64 KB $png->setXMP($xmp); // PNG iTXt $webp->setXMP($xmp); // WebP XMP chunk (adds VP8X)
File Info: one edit, three stores
use Prado\IO\Image\Meta\TFileInfo; $info = TFileInfo::fromJpeg($jpeg); // merged view: XMP, then IPTC, then EXIF $info['title'] = 'Sunset over Bergen'; $info['copyrightstatus'] = TFileInfo::Copyrighted; $info->applyTo($jpeg); // EXIF + XMP + IRB/IPTC updated together $jpeg->save('photo-out.jpg');
GIF animation, frame by frame
Frames are the ones the file actually stores — sub-rectangles, disposal methods and all — so editing metadata or one frame's pixels leaves every other byte untouched.
use Prado\IO\Image\TGIF; use Prado\IO\Image\GIF\TGIFFrame; $gif = TGIF::fromFile('animation.gif'); $gif->getFrameCount(); // 12 $gif->getLoopCount(); // 0 = forever, null = play once $gif->getComments(); // ['made with prado-image'] $frame = $gif->getFrame(0); [$frame->getLeft(), $frame->getTop()]; // the sub-rectangle, not the canvas $frame->getDelayTime(); // hundredths of a second $frame->getDisposalMethod(); // TGIFFrame::DisposalRestoreBackground $frame->getTransparentIndex(); // palette index, or null $frame->getPixels(); // one index byte per pixel, de-interlaced $frame->setDelayTime(8); // retime the animation $gif->setLoopCount(0); // loop forever $gif->save('animation-out.gif'); // every other byte identical
Frames convert to and from images through the graphics seam, quantizing on the way in:
$image = $gif->getFrameImage(0); // \GdImage or \Imagick $frame = new TGIFFrame(); $frame->setDelayTime(10); $frame->setImage($photo); // quantized to a local color table $gif->addFrame($frame); $still = TGIF::fromImage($photo); // a single-frame GIF from any image
Because unknown blocks round-trip raw with their identity intact, a GIF's XMP DataXMP
and ICCRGBG1012 application extensions survive an edit — which is not true of every
GIF library.
PNG: every carrier, and the pixels
PNG reads and writes each carrier the format defines. There is no IPTC chunk, so the
records travel inside the Photoshop image-resource block of the Raw profile type 8bim
text chunk — the form ImageMagick, Photoshop, and ExifTool exchange.
use Prado\IO\Image\TPNG; use Prado\IO\Image\TImageChunk; use Prado\IO\Image\PNG\TPNGChunkType; $png = TPNG::fromFile('image.png'); $png->setICCProfile($iccBytes); // deflated iCCP chunk $png->setEXIF($exif); // eXIf chunk (bare TIFF) $png->setXMP($xmp); // iTXt XMP packet $png->setIPTC($iptc); // stored in the 8BIM text chunk $png->getICCProfile(); // and each reads straight back $png->getPhotoshopIRB(); // the resource block the IPTC rides in // Raw chunk access keeps the spec's normative order and recomputes each CRC. $png->setChunk(new TImageChunk(TPNGChunkType::Gamma, 4, 0, pack('N', 45455))); $png->removeChunk(TPNGChunkType::Text); $image = $png->getImage(); // decode the raster $png->setImage($resized); // re-encode, carrying the metadata chunks across $png->save('image-out.png');
Animated PNG frames are authored, decoded, and edited like GIF frames:
use Prado\IO\Image\PNG\TAPNGFrame; $apng = TPNG::fromApngImages([$frame0, $frame1, $frame2], 0.15, 0); // 0.15s each, loop forever $apng->getIsAnimated(); // true $apng->getFrameCount(); // 3 $apng->getPlayCount(); // 0 = forever $frames = $apng->getApngFrames(); // TAPNGFrame[] with fcTL geometry + delay $frames[1]->setDelaySeconds(0.5); $frames[1]->setDisposeOp(TAPNGFrame::DisposeBackground); $apng->setApngFrames($frames); // rebuilds acTL/fcTL/fdAT, renumbering sequences $image = $apng->getApngFrameImage(0); // decode one frame to a \GdImage or \Imagick $apng->addApngFrame($extra, 0.2); // append a frame from an image $apng->save('animation.png'); // a still viewer shows the default image
WebP: metadata in an extended-format container
WebP carries ICCP, EXIF, and XMP chunks; setting any of them promotes a simple
file to the extended VP8X form and sets the matching feature flag. It defines no IPTC
carrier, so setIPTC() refuses rather than dropping the records.
use Prado\IO\Image\TWebP; $webp = TWebP::fromFile('image.webp'); $webp->setICCProfile($iccBytes); // adds VP8X + the ICC flag if absent $webp->setEXIF($exif); $webp->setXMP($xmp); $webp->getEXIF()?->getValueByName('Model'); try { $webp->setIPTC($iptc); // throws: WebP has no IPTC carrier } catch (\Prado\Exceptions\TIOException $e) { // put the equivalent properties in XMP instead } $webp->setImage($resized, 80); // re-encode the bitstream, metadata carried across $webp->save('image-out.webp');
GD or ImageMagick
Every method that takes or returns a raster accepts \GdImage and \Imagick alike, and builds whichever is asked for. GD is the default when both are loaded; TImageGraphics::setDefaultMode() changes that.
use Prado\IO\Image\IImageGraphicsLibrary; use Prado\IO\Image\TImageGraphics; use Prado\IO\Image\TImageGraphicsMode; $jfif->setImage($imagickThumbnail); // an \Imagick source works anywhere $gd = $jfif->getImage(TImageGraphicsMode::GD); // ... and either library comes back out $im = $jfif->getImage(TImageGraphicsMode::Imagick); TImageGraphics::setDefaultMode(TImageGraphicsMode::Imagick); // prefer ImageMagick for this app $image = $jpeg->getIPTC()->getRasterizedCaptionImage(); // now an \Imagick by default
The backends are not interchangeable for every job, so ask before assuming: supports() answers per library, and never throws for one that is not installed.
TImageGraphics::supports(IImageGraphicsLibrary::CapabilityCmyk); // the default library TImageGraphics::supports(IImageGraphicsLibrary::CapabilityWebP, TImageGraphicsMode::GD); TImageGraphics::getLibrary(TImageGraphicsMode::Imagick)->supports(IImageGraphicsLibrary::CapabilityHighBitDepth); $webp = TImageGraphics::encode($image, IImageGraphicsLibrary::FormatWebP, 80); // false when unsupported $png = TImageGraphics::encode($image, IImageGraphicsLibrary::FormatPng); // lossless: quality ignored $cmyk = TImageGraphics::cmykPixels($image); // 4 bytes/pixel $image = TImageGraphics::fromCmykPixels($cmyk, $w, $h); $profile = TImageGraphics::getICCProfile($image); // ImageMagick only; null under GD TImageGraphics::setICCProfile($image, $profile); // over an existing profile: a transform TImageGraphics::transformICCProfile($image, $sRgb, $adobeRgb); // works in either backend
Carrying a profile on the image object needs ImageMagick — GD has nowhere to keep one and drops any profile on decode, so its getICCProfile() answers null and setICCProfile() false rather than pretending. Converting pixels between two spaces works in both: ImageMagick uses its color-management library (and handles lookup-table profiles), GD uses TICCTransform. When an ability matters more than the library, ask for the library that has it:
$library = TImageGraphics::getCapableLibrary(IImageGraphicsLibrary::CapabilityICCEmbed); $image = $library?->fromRgbPixels($rgb, $w, $h); // built where the profile can ride along
Image codecs
use Prado\IO\Compression\TGIFLZWCompressor; use Prado\IO\Compression\TPackBitsCompressor; $lzw = new TGIFLZWCompressor(); $bytes = $lzw->compress($indexStream, 8); // GIF image data sub-blocks $data = $lzw->decompress($bytes, 8); $packed = (new TPackBitsCompressor())->compress($scanline);
The TLZWFilter, TPackBitsFilter, and THorizontalPredictorFilter classes are the streaming counterparts, attachable to any framework stream as TStreamCodecFilters.
RIFF containers directly
use Prado\IO\Image\TRIFF; $riff = TRIFF::fromFile('audio.wav'); $riff->getFormType(); // 'WAVE' foreach ($riff->getChunks() as $chunk) { [$chunk->getType(), $chunk->getSize(), $chunk->getOffset()]; }
Development
composer install composer unittest # tests vendor/bin/php-cs-fixer fix src # code style (tabs) vendor/bin/php-cs-fixer fix tests # (the finder excludes tests/, so name it) vendor/bin/phpstan analyse --memory-limit=1G # static analysis (level 4)
Coverage is gated rather than merely reported, at two depths. Lines (99.92%) are checked on every push; every source file must be complete except a handful whose remaining lines no test can reach, each justified in AGENTS.md:
XDEBUG_MODE=coverage vendor/bin/phpunit --testsuite unit --coverage-clover build/logs/clover.xml php tests/test_tools/coverage-gate.php build/logs/clover.xml
Branches (99.67%) are checked nightly, because instrumenting every branch takes far
longer than the suite itself. It is the stronger measure: a covered line still hides a
decision that only ever goes one way. The twenty branches that remain are unreachable by
construction — mostly edges PHP emits itself, such as the implicit UnhandledMatchError of a
match whose subject is already range-checked:
XDEBUG_MODE=coverage php -d memory_limit=10G vendor/bin/phpunit --testsuite unit \
--path-coverage --coverage-php build/logs/coverage.php
php -d memory_limit=8G tests/test_tools/branch-gate.php build/logs/coverage.php
Tests cover the format readers (dimensions and invalid-input rejection for JPEG/PNG/WebP, all three WebP variants, PNG ICC inflation); the read-write-every-carrier matrix across JPEG/PNG/WebP/TIFF/GIF (each container round-trips or explicitly refuses every metadata carrier its format defines, including the raster getImage()/setImage() paths); the GIF container (byte-faithful round trips of an animation exercising the whole standard, frame and extension editing, interlace, loop count, application-identity case, quantized import, and malformed-block rejection — cross-checked by decoding the composed files with GD and ImageMagick); the TIFF raster forms (tiles, planar, every bit depth, and the CMYK/YCbCr/Lab photometrics); the private-spaces bridge (reserved/free-space stream views that keep the maker notes byte-identical under Clip/Fail/Skip writes); the ICC profile coder and its pure-PHP color transform; the full XMP value grammar and schema registry; every makernote maker variant; the tag-interpretation decoders; the Photoshop resource decoders; the IPTC record set (parsing, tag-name and id access, validation/coercion, re-encode round trips); the Photoshop 8BIM codec (string and stream detection, IPTC decode/encode); the graphics seam (RGB and CMYK round trips, JPEG/PNG/WebP encode/decode, resampling, mono reduction, palette quantization, and the capability query in both libraries); and the compression codecs (LZW, GIF LZW, PackBits, CCITT fax, and predictor round trips plus their streaming filters). Test images and ICC profiles are generated in memory, so the repository carries no binary fixtures; the Imagick-path tests skip cleanly where ext-imagick is absent, and one CI job runs the whole suite without it to keep that promise honest. Test data is deterministic — PseudoRandomBytes stands in for random_bytes() — because random input makes both assertions and coverage vary from run to run.
Note: the classes use the framework's IO stream layer (
TStream,TLimitStream,TResourceType) and thePrado\IO\Compression\ICompressorcontract. Both are onpradosoft/pradomaster but not yet in a tagged release, so^4.4resolves through the framework'sdev-masterbranch alias for now, and picks up the 4.4 release as soon as it is tagged.
License
BSD-3-Clause. See LICENSE.