popphp / pop-pdf
PHP PDF library for generating and importing PDF documents. A component of the Pop PHP Framework
Requires
- php: >=8.4.0
- popphp/pop-color: ^2.0.0
- popphp/pop-css: ^3.0.0
- popphp/pop-dom: ^5.0.0
- popphp/pop-utils: ^3.0.0
Requires (Dev)
- ext-imagick: *
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^12.5.0
Suggests
- ext-gd: For added images to PDFs
- ext-iconv: For string conversion
- ext-imagick: For advanced image management in PDFs
- ext-mbstring: For working with multi-byte string
- ext-zlib: For compression
Provides
None
Conflicts
None
Replaces
None
- dev-master / 6.2.x-dev
- 6.2.0
- 6.1.1
- 6.1.0
- 6.0.15
- 6.0.14
- 6.0.13
- 6.0.12
- 6.0.11
- 6.0.10
- 6.0.9
- 6.0.8
- 6.0.7
- 6.0.6
- 6.0.5
- 6.0.4
- 6.0.3
- 6.0.2
- 6.0.1
- 6.0.0
- 5.2.12
- 5.2.11
- 5.2.10
- 5.2.9
- 5.2.8
- 5.2.7
- 5.2.6
- 5.2.5
- 5.2.4
- 5.2.3
- 5.2.2
- 5.2.1
- 5.2.0
- 5.1.1
- 5.1.0
- 5.0.0
- 4.2.2
- 4.2.1
- 4.2.0
- 4.1.0
- 4.0.0
- 4.0.0-beta
- 3.2.2
- 3.2.1
- 3.2.0
- 3.1.1
- 3.1.0
- 3.1.0-beta6
- 3.1.0-beta5
- 3.1.0-beta4
- 3.1.0-beta3
- 3.1.0-beta2
- 3.1.0-beta1
- 3.0.4
- 3.0.3
- 3.0.2
- 3.0.1
- 3.0.0
- 2.1.0
- 2.0.0p2
- 2.0.0p1
- 2.0.0
- dev-dev
- dev-hotfix
This package is auto-updated.
Last update: 2026-09-02 02:58:56 UTC
README
- Overview
- Install
- Quickstart
- Documents
- Pages
- Fonts
- Text
- Styles
- Images
- Paths
- Annotations
- Forms
- HTML
Overview
Pop PDF is a robust PDF processing component that's simple to use. With it, you can create PDF documents from scratch, or import existing ones and add to or modify them. It supports embedding images, fonts and URLs, as well as a set of drawing, effect and type features.
pop-pdf is a component of the Pop PHP Framework.
Install
Install pop-pdf using Composer.
composer require popphp/pop-pdf
Or, require it in your composer.json file
"require": {
"popphp/pop-pdf" : "^6.2.0"
}
Quickstart
Create a simple PDF
Create a simple 1-page PDF document with the text "Hello World" on the page. The page size will be letter. The text string will be positioned (50, 50) from the top left and use the standard Arial font.
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Text; $document = new Document(); $document->addFont(Font::ARIAL); $page = $document->createPage(Page::LETTER); $page->addText(new Text('Hello World', 12), Font::ARIAL, 50, 742); Pdf::writeToFile($document, 'my-document.pdf');
Embed an image
Using the same example from above, let's add an image to it:
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Text; use Pop\Pdf\Document\Page\Image; $document = new Document(); $document->addFont(Font::ARIAL); $page = $document->createPage(Page::LETTER); $page->addText(new Text('Look at this image:', 12), Font::ARIAL, 50, 742); $page->addImage(Image::createImageFromFile('my-image.jpg'), 50, 380); Pdf::writeToFile($document, 'my-document.pdf');
The PDF format specification is a vast and comprehensive format that has been around for a
long time. It is comprised of other various media specifications such as fonts and images.
The pop-pdf attempts to present all of these various components in an intuitive, object-oriented
way so that a developer can assemble, build and compile valid PDF documents programmatically.
The main Pop\Pdf\Pdf class serves as a simple processing class with a set of static methods
to route the various object components to the right place to be processed.
writeToFile($document, $filename = 'pop.pdf'): voidoutputToHttp($document, $filename = 'pop.pdf', $forceDownload = false, $headers = []): voidimportFromFile($file, $pages = null, $password = null): AbstractDocumentimportRawData($data, $pages = null, $password = null): AbstractDocumentimportFromImages($images, $quality = 70): AbstractDocumentextractAsImages($file, $location, $format = 'jpg', $resolution = 300, $filenameFormat = '%1$s-%2$02d', $pages = null, $pageLimit = null): arraymerge($files, $document = new Document(), $passwords = []): AbstractDocumentmergeRawData($data, $document = new Document(), $passwords = []): AbstractDocumentextractTextFromFile($file, $pages = null, $pageLimit = null, $password = null): stringextractTextFromData($data, $pages = null, $pageLimit = null, $password = null): stringisImageOnlyDocument($file, $pages = null, $pageLimit = null, $password = null): boolisImageOnlyData($data, $pages = null, $pageLimit = null, $password = null): boolgetImageOnlyPages($file, $pages = null, $pageLimit = null, $password = null): arraygetImageOnlyPagesFromData($data, $pages = null, $pageLimit = null, $password = null): array
Write to File
Once a PDF document has been assembled, you can pass it to the writeToFile() method to
compile the PDF and save it to a file on disk:
use Pop\Pdf\Pdf; // Pass a valid document object and a path/filename Pdf::writeToFile($document, 'path/to/my-document.pdf');
Output to HTTP
Alternatively, you can push the PDF document out to an HTTP client. Giving it a filename
sets the Content-Disposition filename value. Setting $forceDownload to true sets
the Content-Disposition value to "attachment" to force a download (vs display inline.)
A fourth $headers parameter is available to output any additional HTTP headers.
use Pop\Pdf\Pdf; // Pass a valid document object and a path/filename Pdf::outputToHttp($document, 'my-document.pdf', true);
Import from File
You can take an existing PDF and import it to add new content to it. It will translate the PDF document's content into the appropriate objects such as pages, fonts, images and text. From there, you can add more content to the PDF document object and save it.
use Pop\Pdf\Pdf; $doc = Pdf::importFromFile('path/to/document.pdf');
You can also choose which pages of a PDF document to import:
use Pop\Pdf\Pdf; // Import pages 2, 4 and 6 from the PDF document $doc = Pdf::importFromFile('path/to/document.pdf', [2, 4, 6]);
Import from Raw Data
If you have a stream of raw data from a PDF file, you can import that as well. This method supports optional page selection as well
use Pop\Pdf\Pdf; $doc = Pdf::importRawData($rawData, [2, 4, 6]);
Import from Images
If you have an array of images, you can convert them into a PDF document object where each image becomes a page in the PDF document.
use Pop\Pdf\Pdf; $doc = Pdf::importFromImages($arrayOfImages);
Extract as Images
The opposite of importing from images: extractAsImages() rasterizes the pages of an existing PDF
into standalone image files, one per page, via Imagick.
It requires the ext-imagick PHP extension (listed as a suggested dependency, not a hard requirement,
since it isn't installed everywhere) and works regardless of whether a page is text, vector graphics,
a scanned image, or any mix — Imagick's PDF delegate rasterizes the page's full content directly.
use Pop\Pdf\Pdf; // Writes one image per page into 'path/to/output/', e.g. document-01.jpg, document-02.jpg, etc. $images = Pdf::extractAsImages('path/to/document.pdf', 'path/to/output/'); // $images is keyed by 1-based page number: // [1 => 'path/to/output/document-01.jpg', 2 => 'path/to/output/document-02.jpg', ...]
$format (jpg, jpeg, png, webp, tiff or tif) and $resolution (output DPI, defaulting to 300 - a
good baseline for OCR) can be customized, and the same $pages/$pageLimit filters as the text-extraction
methods below are supported:
use Pop\Pdf\Pdf; // Extract pages 1 and 3 only, as PNGs $images = Pdf::extractAsImages('path/to/document.pdf', 'path/to/output/', 'png', 300, pages: [1, 3]);
$filenameFormat is a sprintf() format string given the source file's basename and the 1-indexed page
number, letting you control the output filenames without the basename or with different separators/padding:
// 'page-01.jpg', 'page-02.jpg', ... $images = Pdf::extractAsImages('path/to/document.pdf', 'path/to/output/', filenameFormat: 'page-%2$02d'); // 'page_1.jpg', 'page_2.jpg', ... (no zero-padding) $images = Pdf::extractAsImages('path/to/document.pdf', 'path/to/output/', filenameFormat: 'page_%2$d');
JPEG and WebP output (both lossy by default) are written at a fixed high compression quality (90) to avoid artifacts that hurt OCR accuracy; PNG and TIFF are already lossless and unaffected.
Each requested page is rasterized and written to disk individually, so memory use stays flat regardless of how many pages the source PDF has.
Merge
You can merge multiple, separate PDF files into a single document object. Each source file's pages are appended in order:
use Pop\Pdf\Pdf; $doc = Pdf::merge(['path/to/one.pdf', 'path/to/two.pdf', 'path/to/three.pdf']); Pdf::writeToFile($doc, 'merged.pdf');
You can also merge from an array of raw PDF data streams instead of file paths:
use Pop\Pdf\Pdf; $doc = Pdf::mergeRawData([$pdfStream1, $pdfStream2]);
If you already have a Document object you've started building - with its own fonts, metadata or
pages already added - pass it as the second argument, and the merged files' pages are added into it
instead of a new document being created:
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; $document = new Document(); $document->addFont(Font::ARIAL); $doc = Pdf::merge(['path/to/one.pdf', 'path/to/two.pdf'], $document); // $doc is the same $document instance, with the merged pages appended
If the Document you pass in already has pages of its own, they're placed before the merged
files' pages in the resulting document, regardless of the order in which they were added.
Extract Text
If you just want to extract the text from a PDF that contains text (not a PDF comprised of images with text in them), you can do so like this:
use Pop\Pdf\Pdf; $text = Pdf::extractTextFromFile('path/to/document.pdf');
use Pop\Pdf\Pdf; $text = Pdf::extractTextFromData($pdfStream);
Text extraction is entirely native to pop-pdf — including PDFs that use embedded fonts, not just
the 14 standard PDF fonts — so no third-party PDF-parsing library is required.
Both methods also accept an optional $pages parameter (an int or array of 1-based page numbers) to
extract text from only certain pages, and an optional $pageLimit to cap how many pages are walked —
useful to bound processing time/memory on very large documents:
use Pop\Pdf\Pdf; // Extract text from pages 1, 2 and 3 only $text = Pdf::extractTextFromFile('path/to/document.pdf', [1, 2, 3]); // Extract text, but never walk past the first 50 pages $text = Pdf::extractTextFromFile('path/to/document.pdf', null, 50);
Image-Only Detection
You can determine whether a PDF's pages are nothing but a single scanned/drawn image with no extractable text — useful for routing a document to OCR before attempting text extraction on it.
use Pop\Pdf\Pdf; if (Pdf::isImageOnlyDocument('path/to/document.pdf')) { // every page is just a single full-page image - send it to OCR }
isImageOnlyData() performs the same check against a raw PDF data stream instead of a file:
use Pop\Pdf\Pdf; $isImageOnly = Pdf::isImageOnlyData($pdfStream);
Both accept the same optional $pages and $pageLimit parameters as the text-extraction methods above.
If you need a per-page breakdown rather than a single whole-document answer, use getImageOnlyPages()
(or getImageOnlyPagesFromData() for raw data). Each returns an array keyed by 0-based page index, with
a boolean value indicating whether that specific page is image-only:
use Pop\Pdf\Pdf; $pages = Pdf::getImageOnlyPages('path/to/document.pdf'); // [0 => true, 1 => false, 2 => true]
Documents
The document object serves as the main collection object of all of the components that go into building and compiling a PDF document. This includes pages, fonts and forms.
Compression
A PDF document can be compressed if needed to attempt to reduce file size.
use Pop\Pdf\Document; $document = new Document(); $document->setCompression(true);
Page Origin
A potentially confusing aspect of PDF documents is that the default page origin is the bottom left. This means that all coordinates and any math based on the coordinates has to be calculated from the bottom left.
If you'd prefer to calculate the origin from a different place, you can set that with the setOrigin()
method on the document object. This will automatically translate your preferred origin to the native
PDF origin.
Options for setting the origin of the document are:
ORIGIN_TOP_LEFTORIGIN_TOP_RIGHTORIGIN_BOTTOM_LEFTORIGIN_BOTTOM_RIGHTORIGIN_CENTER
use Pop\Pdf\Document; $document = new Document(); $document->setOrigin(Document::ORIGIN_TOP_LEFT);
Encryption & Password Protection
A document can be password-protected and encrypted with Document\Security, using either
AES-128 or AES-256 (the default). Setting a Document\Permissions object on the security
object restricts what a PDF reader/viewer will allow once the document is opened with the
user password - printing, copying, form filling, and so on - each defaulting to allowed
unless turned off.
use Pop\Pdf\Document\Security; use Pop\Pdf\Document\Permissions; $security = new Security('open-me', 'admin123'); $security->setPermissions((new Permissions())->allowPrinting(false)->allowCopying(false)); $document->setSecurity($security); Pdf::writeToFile($document, 'protected.pdf');
The first constructor argument is the user (open) password, required to open the document at
all; the second is the owner password, which grants full access regardless of the permissions
above. Either may be omitted (null) - an owner password left unset is generated automatically
so the permissions still can't be bypassed by leaving it blank.
When a document has security set, Pop\Pdf\Build\Compiler encrypts every stream in the
document: page content streams, embedded images, and embedded font files. This works with or
without setCompression(true) - compression runs first and encryption wraps whatever bytes the
filter chain produced.
When a document has security set, every literal string this library itself authors into that
document is also encrypted - /Info metadata, annotation URLs, form-field
names/values/tooltips/appearance strings, and an embedded font's internal /CIDSystemInfo
entries. This matches the /StrF /StdCF declaration in the /Encrypt dictionary, which is what
every mainstream PDF reader expects and requires for correct decryption - a document declaring
encrypted strings while actually leaving some plaintext would cause a conforming reader to
"decrypt" (and thereby corrupt) that plaintext.
This coverage applies to content this library builds itself via the Document\*/Build\* API
described throughout this README. It does not extend to literal strings that arrive verbatim
from an imported or merged source PDF - see the known limitation under
Reading Encrypted PDFs.
Reading Encrypted PDFs
Opening an already-encrypted PDF requires the correct password, passed as an additional argument to whichever import/merge/extract method you're using:
use Pop\Pdf\Document; use Pop\Pdf\Pdf; $document = Pdf::importFromFile('protected.pdf', null, 'open-me'); $text = Pdf::extractTextFromFile('protected.pdf', null, null, 'open-me'); $merged = Pdf::merge(['a.pdf', 'protected.pdf'], new Document(), [1 => 'open-me']);
Either the User or Owner password works to open a document - PDF's Standard Security Handler doesn't distinguish which one you present for that purpose, only for which permissions apply afterward (which pop-pdf doesn't enforce at read time - see the write-side documentation above).
Opening a source PDF is fully transparent to everything downstream: the resulting
in-memory Document has no memory of having been encrypted. Writing it back out (including
via merge(), which can combine an encrypted source with plain ones) produces an
unencrypted PDF unless you explicitly call setSecurity() again yourself.
Known limitation: calling setSecurity() on a Document built (in whole or in part) via
importFromFile(), importRawData(), merge(), or mergeRawData() currently encrypts every
stream in the output, and every literal string this library itself authors (/Info,
annotations, form fields, embedded fonts it builds) - but it does not encrypt literal strings
that were carried over verbatim from the imported/merged source PDF, such as an annotation's
/URI or a form field's /T//V from the original document. Those source-authored strings are
written back out unencrypted even though the resulting file's /Encrypt dictionary declares
/StrF /StdCF (all strings encrypted) - a conforming PDF reader will try to AES-decrypt that
plaintext as if it were real ciphertext, corrupting it. This is a disclosed, known gap rather
than a silent bug - closing it requires a general-purpose literal-string scanner over
already-serialized imported object text, which is planned for a future release - so until then,
avoid calling setSecurity() on an imported or merged document that contains literal strings you
need protected, and treat any such strings in the output as unprotected.
Note that a source PDF's encrypted literal strings are not decrypted on the way in: no
string-decode layer exists anywhere in the extraction/import pipeline today, so any literal
string value read from an encrypted source (regardless of which tool wrote it, including this
library's own write path read back in) comes back as raw ciphertext bytes. Content, images, and
fonts still import successfully. Because those bytes are not readable metadata in any useful
sense, importing an encrypted document deliberately does not copy its /Info dictionary
(title, author, subject, creator, producer, dates) into the resulting Document's Metadata -
the imported document gets the default metadata instead.
Unsupported encryption configurations
Only AES-128 (/AESV2, revision 4) and AES-256 (/AESV3, revision 6) can be opened. A PDF
using any of the following raises a clear exception naming the problem rather than failing
mysteriously - if you hit one, these are the terms to search for:
| Configuration | Status |
|---|---|
RC4 (/V 1 or 2, revision 2 or 3) |
Not supported - rejected as an unsupported encryption configuration |
| Revision 5 (Adobe extension level 3, deprecated) | Not supported - rejected as an unsupported encryption configuration |
/EncryptMetadata false (e.g. qpdf's --cleartext-metadata) |
Supported for AES-128 and AES-256, unless the document also carries an XMP /Metadata stream (see below) |
Encrypted strings (a non-/Identity /StrF) |
Opens; strings come back as ciphertext, and /Info is skipped (see above) |
RC4 is deliberately absent rather than merely unimplemented: it is broken, and qpdf 11+ won't
even write it without --allow-weak-crypto. A damaged encrypted PDF whose cross-reference
data can't be repaired well enough to locate its /Encrypt dictionary is also refused
outright, rather than being reported as an unencrypted document with empty page content.
A document with /EncryptMetadata false and an XMP /Metadata stream - the actual reason
anyone sets that flag - fails to open: /EncryptMetadata false means that one stream is left
unencrypted, but nothing in this library's read path knows to skip decrypting it, so opening
one raises Could not decrypt the stream of object N: AES-128/256 decryption failed instead
of succeeding.
Pages
Pages can be virtually any size, but there are a number of pre-defined sizes available
as constants in the Pop\Pdf\Document\Page class:
| Page | (W x H) | Page | (W x H) | Page | (W x H) |
|---|---|---|---|---|---|
ENVELOPE_10 |
(297 x 684) | A1 |
(1684 x 2384) | B1 |
(2064 x 2920) |
ENVELOPE_C5 |
(461 x 648) | A2 |
(1191 x 1684) | B2 |
(1460 x 2064) |
ENVELOPE_DL |
(312 x 624) | A3 |
(842 x 1191) | B3 |
(1032 x 1460) |
FOLIO |
(595 x 935) | A4 |
(595 x 842) | B4 |
(729 x 1032) |
EXECUTIVE |
(522 x 756) | A5 |
(420 x 595) | B5 |
(516 x 729) |
LETTER |
(612 x 792) | A6 |
(297 x 420) | B6 |
(363 x 516) |
LEGAL |
(612 x 1008) | A7 |
(210 x 297) | B7 |
(258 x 363) |
LEDGER |
(1224 x 792) | A8 |
(148 x 210) | B8 |
(181 x 258) |
TABLOID |
(792 x 1224) | A9 |
(105 x 148) | B9 |
(127 x 181) |
A0 |
(2384 x 3370) | B0 |
(2920 x 4127) | B10 |
(91 x 127) |
use Pop\Pdf\Document; use Pop\Pdf\Document\Page; $pageLetter = new Page(Page::LETTER); $pageCustom = new Page(500, 1000); // Custom width and height $document = new Document(); $document->addPages([$pageLetter, $pageCustom]);
Alternatively, you can use the document object as a page factory, which will create a page object, automatically add the page to the document object and return the new page:
use Pop\Pdf\Document; use Pop\Pdf\Document\Page; $document = new Document(); $pageLegal = $document->createPage(Page::LEGAL);
There are a number of other methods within the document object to assist with managing various components:
addPage(Page $page): DocumentaddPages(array $pages): DocumentcreatePage(mixed $size, ?int $height = null): PagecopyPage(int $p, bool $preserveContent = true): PageorderPages(array $pages): DocumentdeletePage(int $p): DocumentaddFont(Font|string $font, bool $embedOverride = false): DocumentembedFont(Font $font, bool $embedOverride = false): DocumentsetCurrentPage(int $p): DocumentsetCurrentFont(string $name): Document
Fonts
Fonts are required to be added to a document for any text that might be added to any page. The font that a text object uses will be defined when adding the text to a page object, but that font will need to be present in the document object. Once fonts are added to a document, they can be used repeatedly by any text objects on any pages of the document.
There are two types of supported fonts: standard and embedded.
Standard
Part of the PDF specification is that a total of 25 standard fonts that are supported by PDF and PDF readers. This means that no additional font files have to be embedded and the fonts are available by default.
| Standard PDF Fonts | ||
|---|---|---|
| Arial | CourierNew,Bold | Times-Bold |
| Arial,Italic | Courier-BoldOblique | Times-Italic |
| Arial,Bold | CourierNew,BoldItalic | Times-BoldItalic |
| Arial,BoldItalic | Helvetica | TimesNewRoman |
| Courier | Helvetica-Oblique | TimesNewRoman,Italic |
| CourierNew | Helvetica-Bold | TimesNewRoman,Bold |
| Courier-Oblique | Helvetica-BoldOblique | TimesNewRoman,BoldItalic |
| CourierNew,Italic | Symbol | ZapfDingbats |
| Courier-Bold | Times-Roman |
References to each of these standard fonts are available as constants on the main
font class, Pop\Pdf\Document\Font:
Font::ARIALFont::ARIAL_ITALICFont::ARIAL_BOLDFont::ARIAL_BOLD_ITALICFont::COURIERFont::COURIER_OBLIQUEFont::COURIER_BOLDFont::COURIER_BOLD_OBLIQUEFont::COURIER_NEWFont::COURIER_NEW_ITALICFont::COURIER_NEW_BOLDFont::COURIER_NEW_BOLD_ITALICFont::HELVETICAFont::HELVETICA_OBLIQUEFont::HELVETICA_BOLDFont::HELVETICA_BOLD_OBLIQUEFont::SYMBOLFont::TIMES_ROMANFont::TIMES_BOLDFont::TIMES_ITALICFont::TIMES_BOLD_ITALICFont::TIMES_NEW_ROMANFont::TIMES_NEW_ROMAN_ITALICFont::TIMES_NEW_ROMAN_BOLDFont::TIMES_NEW_ROMAN_BOLDITALICFont::ZAPF_DINGBATS
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Text; $document = new Document(); $document->addFont(Font::HELVETICA_BOLD); $page = $document->createPage(Page::LETTER); $page->addText(new Text('Hello World', 12), Font::HELVETICA_BOLD, 50, 742); Pdf::writeToFile($document, 'my-document.pdf');
Embedded
If you require a font outside of the set of standard fonts, the PDF specification supports embedding a number of different external font formats:
- TrueType (ttf)
- OpenType (otf)
- Type1 (pfb)
Most fonts of these types should work, but there are situations were the font may not be parsable, such as when a font's embeddable flag is set to false.
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Page; $font = new Font('/path/to/some/font.ttf'); $document = new Document(); $document->embedFont($font); $page = new Page(Page::LETTER); $page->addText(new Page\Text('Hello World', 36), $font->getName(), 50, 600); $document->addPage($page); Pdf::writeToFile($document, 'my-document.pdf');
Unicode and embedded fonts
Embedded TrueType/OpenType fonts are compiled into the PDF as composite
(/Type0 + /Identity-H + /CIDFontType2) fonts, with a /ToUnicode CMap so the
resulting text still copies and extracts correctly. Text is emitted as glyph-ID hex
strings rather than single-byte literals.
The practical effect is that any script the embedded font has glyphs for now renders
correctly — Cyrillic, Greek, and so on — instead of the mojibake produced by the old
single-byte (/WinAnsiEncoding) output:
$font = new Font('/path/to/DejaVuSans.ttf'); // a Unicode-capable font $document = new Document(); $document->embedFont($font); $page = new Page(Page::LETTER); $page->addText(new Page\Text('123 ПРИВІТ:', 36), $font->getName(), 50, 600);
Notes and caveats:
- This changes the compiled byte structure of any document that uses an embedded TrueType/OpenType font. No PHP method signatures changed, but code that diffs or post-processes exact generated PDF bytes will see different (more correct) output.
- Embedded Type1 (
.pfb) fonts and the standard fonts are unchanged — they stay on the single-byte encoding path. - Fonts are embedded in full; there is no glyph subsetting, so a large Unicode font makes for a large PDF.
- Text alignment (
Text\Alignment), box wrapping (Text\Wrap), and character wrapping (Text::setCharWrap()) all work with embedded Unicode fonts and non-Latin text. Character wrap width is measured in characters, not bytes, so multi-byte text (accented Latin, Cyrillic, CJK, etc.) wraps at the intended width instead of wrapping too early.
Unsupported characters now throw
If text contains a character the active font has no glyph for, a
Pop\Pdf\Build\Font\Exception is thrown at compile time, naming the font, the character
and its codepoint:
Error: The font 'Arial' does not contain a glyph for character 'П' (U+041F).
This is a deliberate behavior change: previously such characters were silently written out and rendered as garbage. The standard (base-14) fonts contain no Cyrillic, Greek or CJK glyphs at all, so non-Latin text now requires an embedded Unicode font.
Standard fonts also now correctly render any character their /WinAnsiEncoding cmap
covers — accented Latin letters, curly quotes, en/em dashes, and similar Latin-1/
Windows-1252 punctuation (café, 'quoted', †, etc.). Previously these were silently
written as raw UTF-8 bytes into a single-byte string and mojibaked in any PDF viewer even
though the font could represent them; text is now transcoded to /WinAnsiEncoding before
being written. Only characters truly outside the font's encoding throw.
Also note that importing HTML containing non-Latin text (Pdf::importFromHtml()) does
not currently work, due to a double-encoding bug in the upstream popphp/pop-dom
dependency that mangles non-ASCII text before pop-pdf receives it.
Text
Once font objects have been added to a document object, text objects can then be added to page objects, while referencing the available font objects in the document.
The constructor of the text object takes the string and the size:
use Pop\Pdf\Document\Page\Text; $text = new Text('Hello World', 12);
There are a number of methods to assist in modifying the text object:
setSize(int|float $size): TextsetFillColor(ColorInterface $color): TextsetStrokeColor(ColorInterface $color): TextsetStroke(int $width, ?int $dashLength = null, ?int $dashGap = null): TextsetRotation(int $rotation): TextsetCharWrap(int $charWrap, ?int $leading = null): TextsetLeading(int $leading): Text
A basic character wrap can be set with the setCharWrap() method. The leading of the
wrapped text can be either set with the second parameter or by the setLeading() method.
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Text; $document = new Document(); $document->addFont(Font::ARIAL); $longString = 'Lorem ipsum [...really long string...] anim id est laborum.'; $text = new Text($longString, 12); $text->setCharWrap(80, 16); // Set the wrap at 80 characters and a leading of 16 $page = $document->createPage(Page::LETTER); $page->addText($text, Font::ARIAL, 50, 742); Pdf::writeToFile($document, 'my-document.pdf');
Styles
Style objects can be added to the document to provide easier management of text and font styles used in multiple places across the PDF document and its pages.
use Pop\Pdf\Document; use Pop\Pdf\Document\Font; $document = new Document(); $document->addFont(Font::ARIAL); $document->createStyle('normal', Font::ARIAL, 12); $page = $document->createPage(Page::LETTER); $page->addText($text, 'normal', 50, 742); // The second parameter can either be a font or a reference to a style
So any text added to any page referencing the same style can easily be changed across the entire document by only changing the style object.
A style can also carry a color, applied as the fill color for any text rendered with it:
use Pop\Color\Color; use Pop\Pdf\Document\Style; $document->addStyle(Style::create('heading', Font::ARIAL, 24, new Color\Rgb(0, 102, 204)));
The style's color only affects the text it's applied to at render time - it does not change the fill color used by any text or paths that come after it on the page.
Alignment
Alignment objects are objects that assist with handling more advanced alignment and wrapping of text based on geometric positioning. When creating an alignment object, you define a bounding areas to which the text will be confined.
Left-aligned box
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Text; use Pop\Pdf\Document\Page\Text\Alignment; $document = new Document(); $document->addFont(Font::ARIAL); $longString = 'Lorem ipsum [...really long string...] anim id est laborum.'; $text = new Text($longString, 12); // Create a left-aligned bounding area with the // X between 50 and 350; leading set 16 $text->setAlignment(Alignment::createLeft(50, 350, 16)); $page = $document->createPage(Page::LETTER); $page->addText($text, Font::ARIAL, 50, 742); Pdf::writeToFile($document, 'my-document.pdf');
Right-aligned box
// Create a right-aligned bounding area with the // X between 250 and 550; leading set 16 $text->setAlignment(Alignment::createRight(250, 550, 16));
Center-aligned box
// Create a center-aligned bounding area with the // X between 50 and 350; leading set 16 $text->setAlignment(Alignment::createCenter(200, 412, 16));
String Width
An important and useful tool with working with text and fonts to the ability to calculate the width of a string of characters rendered in a particular font. This is very helpful when attempting to correctly position text on the page.
There is a method on the font object that will allow you pass a string of text to it, as well as the desired size, to give you the approximate width those characters will take up rendered in that font at that size.
This works for both standard and embedded fonts.
use Pop\Pdf\Document\Font; $font = new Font(Font::HELVETICA_BOLD); $width = $font->getStringWidth('Hello World', 12); var_dump($width);
This will give us the approximate width in points of the string Hello World in
12pt Helvetica Bold:
float(66.672)
Images
Images can be easily added to page objects. However, in a PDF document, the origin of an image is the bottom of the image. You will have to consider how the image's height affects the placement of the image on the page in relation to the page origin.
In this example below, the image is 320 x 320. If you place the $y value at 742
(top origin 792 - 50), then only the bottom 50 pixels of the image would display
at the top of the page, while the remainder bleeds off the top page border. Therefore,
the height should be taken into account and the $y value should be a value like 422
(top origin 792 - 50 - 320). This would make the image appear with the top of it starting
at 50 pixels from the top of the page, and you would be able to safely see the entire
image on the page.
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Image; $document = new Document(); $page = $document->createPage(Page::LETTER); $page->addImage(Image::createImageFromFile('my-image.jpg'), 50, 422); Pdf::writeToFile($document, 'my-document.pdf');
In the above example, the image is pulled from a file. You can also import an image from a raw stream:
$page->addImage(Image::loadImageFromStream($imageContents), 50, 422);
As a shortcut, addImage() also accepts a file path directly, creating the Image object
for you:
$page->addImage('my-image.jpg', 50, 422);
Image Size
You can resize a larger image when adding it to a page.
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Image; $image = Image::createImageFromFile('my-image.jpg'); $image->resizeToWidth(120); $document = new Document(); $page = $document->createPage(Page::LETTER); $page->addImage($image, 50, 622); Pdf::writeToFile($document, 'my-document.pdf');
The following methods are available to resize an image:
resizeToWidth(int $width, bool $preserveResolution = false): ImageresizeToHeight(int $height, bool $preserveResolution = false): Imageresize(int $pixel, bool $preserveResolution = false): Imagescale(float $scale, bool $preserveResolution = false): Image
The $preserveResolution flag is set to false by default. This will
resize the image resource, which will reduce it in not only dimensional size,
but also reduce its data size as well.
If you wish to keep the image in its original higher quality, and
only reduce the dimensions, you can set the $preserveResolution flag
to true. This is typically a good method to keep the image clean and crisp
when being reduced to a smaller dimension.
Paths
You can add path objects to a page to draw vector lines and shapes on the page object.
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Path; use Pop\Color\Color; $document = new Document(); $path = new Path(Path::FILL_STROKE); $path->setFillColor(Color::rgb(155, 20, 20)) ->setStrokeColor(Color::rgb(81, 125, 153)) ->setStroke(5) ->drawRectangle(50, 400, 320, 240); $page = new Page(Page::LETTER); $page->addPath($path); $document->addPage($page); Pdf::writeToFile($document, 'my-document.pdf');
The methods to control color and style include:
setFillColor(Color\ColorInterface $color): PathsetStrokeColor(Color\ColorInterface $color): PathsetStroke(int $width, ?int $dashLength = null, ?int $dashGap = null): PathsetStyle(string $style): Path
The setStyle() method can take one of the available style constants as its parameter:
Path::STROKEPath::STROKE_CLOSEPath::FILLPath::FILL_EVEN_ODDPath::FILL_STROKEPath::FILL_STROKE_EVEN_ODDPath::FILL_STROKE_CLOSEPath::FILL_STROKE_CLOSE_EVEN_ODDPath::CLIPPINGPath::CLIPPING_FILLPath::CLIPPING_NO_STYLEPath::CLIPPING_EVEN_ODDPath::CLIPPING_EVEN_ODD_FILLPath::CLIPPING_EVEN_ODD_NO_STYLEPath::NO_STYLE
The basic methods available to draw paths and shapes are:
drawLine(int $x1, int $y1, int $x2, int $y2): PathdrawRectangle(int $x, int $y, int $w, ?int $h = null): PathdrawRoundedRectangle(int $x, int $y, int $w, ?int $h = null, int $rx = 10, ?int $ry = null): PathdrawSquare(int $x, int $y, int $w): PathdrawRoundedSquare(int $x, int $y, int $w, int $rx = 10, ?int $ry = null): PathdrawPolygon(array $points): PathdrawEllipse(int $x, int $y, int $w, ?int $h = null): PathdrawCircle(int $x, int $y, int $w): PathdrawArc(int $x, int $y, int $start, int $end, int $w, ?int $h = null): PathdrawChord(int $x, int $y, int $start, int $end, int $w, ?int $h = null): PathdrawPie(int $x, int $y, int $start, int $end, int $w, ?int $h = null): Path
Each shape paints itself in the path's style as it is drawn.
Composed Paths
Several outlines can be painted together as a single path instead, which is what an even-odd fill needs to cut a hole - the rule reads every outline on the same path to decide what is inside. Open the path, draw the outlines, and paint them in one go:
use Pop\Pdf\Document\Page\Path; use Pop\Color\Color; $path = new Path(Path::FILL_EVEN_ODD); $path->setFillColor(Color::rgb(0, 102, 204)); $path->openPath(); $path->drawPolygon([ ['x' => 150, 'y' => 250], ['x' => 450, 'y' => 250], ['x' => 450, 'y' => 550], ['x' => 150, 'y' => 550] ]); $path->drawPolygon([ ['x' => 250, 'y' => 350], ['x' => 350, 'y' => 350], ['x' => 350, 'y' => 450], ['x' => 250, 'y' => 450] ]); $path->paintPath();
That fills the outer 300-point square and leaves the inner 100-point square blank. Swapping the style
for Path::FILL fills the same two outlines solid.
openPath(): PathpaintPath(): Path
Annotations
Annotation objects provide a way to link to external URLs or an internal pointer within the document.
URLs
The following example will generate an invisible annotation box area over the text
Visit Google that links to Google's home page:
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Text; use Pop\Pdf\Document\Page\Annotation\Url; $document = new Document(); $document->addFont(Font::ARIAL); $page = $document->createPage(Page::LETTER); $page->addText(new Text('Visit Google', 12), Font::ARIAL, 50, 742); $page->addUrl(new Url(100, 15, 'https://www.google.com/'), 50, 742); Pdf::writeToFile($document, 'my-document.pdf');
Internal
The following example will add 2 pages to the document and link from the first page to the second page. When creating an internal link, you can define the following:
- The X and Y coordinates to navigate to
- The Z (Zoom) target
- The page target
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Text; use Pop\Pdf\Document\Page\Annotation\Link; $document = new Document(); $document->addFont(Font::ARIAL); $page1 = $document->createPage(Page::LETTER); $page1->addText(new Text('This is an internal link', 12), Font::ARIAL, 50, 742); $page2 = $document->createPage(Page::LETTER); $page2->addText(new Text('This is the destination', 12), Font::ARIAL, 50, 742); // Create a link to page 2 and set the zoom to 110% $link = new Link(120, 15, 10, 752); $link->setPageTarget(2) ->setZTarget(110); $page1->addLink($link, 50, 742); Pdf::writeToFile($document, 'my-document.pdf');
Forms
Forms and form fields are supported in Pop PDF, however, please note that not all browsers consistently support forms and form fields in their default PDF readers. It is recommended that if you generate a PDF with a form in it using Pop PDF, that your end user views it in an Adobe product.
The types of fields that are currently supported in Pop PDF are:
- Single-line text fields
- Multi-line text fields
- Single-select choice fields (e.g., an HTML select drop-down)
- Multi-select choice fields (e.g., an HTML multi-select drop-down)
- Push buttons (by default, display and act like a checkbox)
- Radio buttons, including true grouped radio-button behavior (see below)
Appearance
AbstractField (the base class Text, Choice, and Button all share) supports optional border and
background appearance, alongside the existing font color support:
$firstName->setBorderWidth(1) ->setBorderColor([153, 153, 153]) // [r, g, b], 0-255 each ->setBackgroundColor([255, 255, 255]);
setBorderWidth() is the gate: a width of 0 (the default) omits both the border style and border
color entirely, matching ordinary CSS semantics. setBorderColor()/setBackgroundColor() each take
an [r, g, b] array (0-255 per channel) and are rendered together as the field's /MK appearance-
characteristics dictionary.
Checkboxes and radio buttons
Button::setValue() always represents the field's export/on-state value - the name a conforming PDF
reader reports for that widget when it IS checked - independent of whether it is checked right now.
Whether a given Button widget is currently checked is tracked by its own, separate flag:
$agree = new Page\Field\Button('agree'); $agree->setValue('Yes'); // the export value used once checked $agree->setChecked(); // actually check it $agree->isChecked(); // => true
Calling setChecked() never changes getValue(), and calling setValue() never implicitly checks
the field - the two are independent, which matters most for a group of radio buttons: every option
needs its own setValue() (even the ones that start out unchecked) so each has its own distinct
export name, while setChecked() is called on whichever single option should start selected.
A group of Button fields that share the same field name and are all marked setRadio() compiles to
a true, grouped PDF radio field - one shared, non-visual parent field plus one child widget per
option - rather than independent checkboxes standing in for a radio group. In a conforming reader,
selecting one option in the group automatically deselects the others.
The following script below demonstrates how to add the various fields to a form in a PDF object. While lengthy, it includes text and graphic support for field names and borders:
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Form; use Pop\Pdf\Document\Page; use Pop\Pdf\Document\Page\Path; use Pop\Pdf\Document\Page\Text; $form = new Form('contact_form'); $document = new Document(); $document->addForm($form); $document->addFont(new Font(Font::ARIAL)); $document->addFont(new Font(Font::ARIAL_BOLD)); $firstName = new Page\Field\Text('first_name'); $firstName->setWidth(200) ->setHeight(20); $lastName = new Page\Field\Text('last_name'); $lastName->setWidth(200) ->setHeight(20); $colors = new Page\Field\Choice('colors'); $colors->addOption('Red') ->addOption('Green') ->addOption('Blue') ->setMultiSelect() ->setWidth(200) ->setHeight(50) ->setFont(Font::ARIAL) ->setSize(11); $city = new Page\Field\Choice('city'); $city->addOption('New Orleans') ->addOption('New York') ->addOption('Los Angeles') ->setCombo() ->setWidth(200) ->setHeight(20) ->setFont(Font::ARIAL) ->setSize(11); $lovePhp = new Page\Field\Button('love_php'); $lovePhp->addOption('PHP')->setWidth(20) ->setHeight(20); $lovePdf = new Page\Field\Button('love_pdf'); $lovePdf->addOption('PDF')->setRadio() ->setWidth(20) ->setHeight(20); $comments = new Page\Field\Text('comments'); $comments->setWidth(500) ->setHeight(150) ->setMultiline(); $page = new Page(Page::LETTER); $page->addText(new Text('First Name:', 14), Font::ARIAL_BOLD, 50, 680); $page->addText(new Text('Last Name:', 14), Font::ARIAL_BOLD, 300, 680); $page->addText(new Text('Favorite Colors?', 14), Font::ARIAL_BOLD, 50, 580); $page->addText(new Text('Favorite City?', 14), Font::ARIAL_BOLD, 300, 580); $page->addText(new Text('Love PHP?', 14), Font::ARIAL_BOLD, 80, 330); $page->addText(new Text('Love PDF?', 14), Font::ARIAL_BOLD, 80, 290); $page->addText(new Text('Comments:', 14), Font::ARIAL_BOLD, 50, 260); $page->addPath((new Path())->drawRectangle(50, 650, 200, 20)); $page->addPath((new Path())->drawRectangle(300, 650, 200, 20)); $page->addPath((new Path())->drawRectangle(50, 520, 200, 50)); $page->addPath((new Path())->drawRectangle(300, 550, 200, 20)); $page->addPath((new Path())->drawSquare(50, 325, 20)); $page->addPath((new Path())->drawCircle(60, 295, 10)); $page->addPath((new Path())->drawRectangle(50, 100, 500, 150)); $page->addField($firstName, 'contact_form', 50, 650) ->addField($lastName, 'contact_form', 300, 650) ->addField($colors, 'contact_form', 50, 520) ->addField($city, 'contact_form', 300, 550) ->addField($lovePhp, 'contact_form', 50, 325) ->addField($lovePdf, 'contact_form', 50, 285) ->addField($comments, 'contact_form', 50, 100); $document->addPage($page); Pdf::writeToFile($document, 'my-document.pdf');
The above code produces a PDF with a form like this:
HTML
HTML rendering is available in pop-pdf, however, please note that while it will attempt preserve the style and flow
of the HTML within the PDF pages, there are limitations in attempting to constrain and conform HTML that renders
fluidly with in a browser window to the boundaries of a PDF page. Support is broadest for the patterns shown below.
Less common CSS properties and nested block markup may not be fully supported yet.
Choosing a page size
Pages created while parsing HTML default to LETTER. To use a different size, pass it as the second
constructor argument - either one of the Page size constants, or a [width, height] array for a
custom size:
use Pop\Pdf\Document; use Pop\Pdf\Document\Page; use Pop\Pdf\Build\Html\Parser; $parser = new Parser(new Document(), Page::A4); // or a custom size: $parser = new Parser(new Document(), [400, 600]);
The parseString(), parseFile() and parseUri() static methods, as well as Pdf::importFromHtml(),
Pdf::importFromHtmlFile() and Pdf::importFromHtmlUri(), all accept the same page size as an optional
argument.
Parsing HTML from a file:
If you have an HTML file, it will parse all of the HTML in it, as well as any linked CSS and images:
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Page; use Pop\Pdf\Build\Html\Parser; $document = new Document(); $document->addFont(Font::ARIAL); $page = $document->createPage(Page::LETTER); $parser = new Parser($document); $parser->parseHtmlFile('test.html'); $parser->process(); Pdf::writeToFile($parser->document(), 'my-document.pdf');
You can also parse HTML and CSS strings directly. The directory path is needed to give the parser a base folder to attempt to access other assets, such as images.
use Pop\Pdf\Pdf; use Pop\Pdf\Document; use Pop\Pdf\Document\Font; use Pop\Pdf\Document\Page; use Pop\Pdf\Build\Html\Parser; $html = <<<HTML <h1>Hello World!</h1> HTML; $css = <<<CSS h1 { font-family: sans-serif; color: #f00; font-weight: normal; } h3 { font-family: serif; color: #009dff; } .red { font-weight: bold; color: #f00; } .img-med { width: 200px; } CSS; $document = new Document(); $document->addFont(Font::ARIAL); $page = $document->createPage(Page::LETTER); $parser = new Parser($document); $parser->parseHtml($html, __DIR__); $parser->parseCss($css); $parser->process(); Pdf::writeToFile($parser->document(), 'my-document.pdf');
CSS can also be embedded directly in the HTML - either in a <style> block or as an inline style="..."
attribute on an element, with inline styles taking precedence over <style>/linked/tag/class/id rules:
$html = <<<HTML <style> h1 { color: #f00; } </style> <h1>Hello World!</h1> <p style="color: #009dff; font-weight: bold;">Inline styles are supported too.</p> HTML; $parser = new Parser($document); $parser->parseHtml($html, __DIR__); $parser->process();
Tables
<table> elements are supported, including colspan/rowspan, and header rows (<thead>, or a row whose
cells are all <th>) that automatically repeat if the table splits across a page break. Column widths are
calculated from cell content, honoring an explicit CSS or HTML width first and distributing the remaining
space proportionally across the rest.
$html = <<<HTML <table> <thead> <tr><th>Item</th><th>Quantity</th><th>Price</th></tr> </thead> <tbody> <tr><td colspan="2">Widgets (assorted)</td><td>$19.99</td></tr> <tr><td rowspan="2">Bulk Order</td><td>100</td><td>$4.50</td></tr> <tr><td>250</td><td>$4.00</td></tr> </tbody> </table> HTML; $css = <<<CSS th { background-color: #dddddd; border-width: 1px; border-color: #333333; } td { border-width: 1px; border-color: #333333; } CSS; $document = new Document(); $document->addFont(Font::ARIAL); $page = $document->createPage(Page::LETTER); $parser = new Parser($document); $parser->parseHtml($html, __DIR__); $parser->parseCss($css); $parser->process(); Pdf::writeToFile($parser->document(), 'my-document.pdf');
border-width/border-color/background-color CSS properties work the same way on any element, not just
table cells (e.g. a bordered <div>). Given a PDF page's fixed size (unlike a browser's scrolling viewport),
a few limitations apply: no border-collapse (each cell's border is drawn independently), no nested tables,
and a single row/cell taller than a full page renders best-effort on that page rather than splitting across a
page break.
Form markup
<form> markup is converted into real, interactive PDF form fields (AcroForms) automatically - there is
no opt-in flag to set. This is a behavior change to be aware of if you are already parsing HTML that
happens to contain form markup: previously that markup was simply inert content and produced nothing, and
it will now produce visible, interactive widgets on the page.
Each control maps onto one of the manual Forms field types described above:
| HTML | Maps to | Notes |
|---|---|---|
input[type=text|email|password|...], textarea |
Field\Text |
type=password → password field; textarea → multiline |
input[type=file] |
Field\Text |
file-select field |
input[type=hidden] |
Field\Text |
zero size, not placed in the page flow; value preserved |
input[type=checkbox] |
Field\Button |
independent, not grouped |
input[type=radio] |
Field\Button (grouped) |
inputs sharing the same name within a <form> become one true, grouped PDF radio field - see Checkboxes and radio buttons above |
select |
Field\Choice |
<option> → a choice option; multiple attribute → multi-select; otherwise a combo box |
button, input[type=submit|reset] |
Field\Button (push) |
cosmetic only - renders (including its caption, from the button's own text or its value attribute) but performs no action, since Pop PDF has no JavaScript/HTTP-submission engine |
any other/unrecognized input[type] |
Field\Text |
falls back rather than throwing |
A control's size comes from CSS (width/height, percentages resolved against the page) or the matching
HTML attribute (size, rows/cols) when present, falling back to a sane fixed default otherwise - the
same precedence <img> already uses. Border/background CSS on a control (border-width, border-color,
background-color) carries through automatically onto the compiled field's appearance, the same way it does
for boxed text and table cells.
A radio group whose options would otherwise straddle a page break is kept together on one page, whenever the whole group fits within a single page's usable height - it forces a page break ahead of the group's first option rather than splitting the group across two pages, since a split group would otherwise compile to two separate, same-named top-level fields instead of one true group.
