Text Layout & Wrapping
A single Text object can lay out an entire paragraph itself, breaking it into lines at whichever
width you give it. A character count and a leading value handle most blocks of text — start with
Character Wrapping below; reach for Alignment or Wrap if a layout needs
exact placement, measured against the font itself.
Character Wrapping#
setCharWrap($charWrap, $leading = null), called directly on Text, breaks a string every
$charWrap characters using PHP's own word-boundary wrapping, without measuring anything against the
font:
use Pop\Pdf\Document\Page\Text;
$text = new Text('Hello World Hello World Hello World Hello World Hello World Hello World', size: 12);
$text->setCharWrap(24, leading: 10);
$text->hasCharWrap(); // true
Because it counts characters rather than rendered width, the same $charWrap value wraps identically
at every font size — coarser than Alignment or Wrap, useful
when font-metric precision isn't worth the setup. hasCharWrap() reports whether a wrap width has
been set.
$leading, setCharWrap()'s second argument and the 10 above, is the vertical distance between
one baseline and the next. It defaults to 0, and if it's still 0 once the lines are laid out at
compile time, Pop PDF fills it in with the text's own font size, so lines fall directly under one
another:
use Pop\Pdf\Document;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Text;
use Pop\Pdf\Document\Font;
$document = new Document(new Page(Page::LETTER));
$document->addFont(Font::ARIAL);
$page = $document->getPage(1);
$text = new Text('Hello World Hello World Hello World Hello World Hello World Hello World', size: 12);
$text->setCharWrap(24, 18);
$text->getLeading(); // 18
$page->addText($text, 'Arial', x: 50, y: 700);
// compiling is what lays the lines out, using the set leading value
$bytes = (string)$document;
Character wrap takes priority over both: a Text with setCharWrap() set ignores any Alignment or
Wrap also set on it and always lays out flush left from the $x given to addText(). Set only one
of the three per Text.
The count is in characters, not bytes, so accented text and CJK break where you would expect, and
any font works — standard or embedded. Alignment and Wrap measure through the font's own metrics
instead, which is the reason to reach for them when a block has to fit a known width.
Counting Wrapped Lines#
getNumberOfWrappedLines() reports how many lines setCharWrap() will break a string into — useful
for reserving vertical space before the text is ever added to a page:
use Pop\Pdf\Document\Page\Text;
$text = new Text('Hello World Hello World Hello World Hello World Hello World Hello World', size: 12);
$text->setCharWrap(24, leading: 10);
echo $text->getNumberOfWrappedLines(); // 3
The count only reflects setCharWrap()'s own wrap, with no relationship to Alignment or Wrap —
calling it before setCharWrap() has a real width returns the count for wrapping at 0 characters,
breaking after every word. Call setCharWrap() first.
Multiplying that count by the leading gives the exact height a character-wrapped block needs:
use Pop\Pdf\Document;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Text;
use Pop\Pdf\Document\Page\Path;
use Pop\Pdf\Document\Font;
use Pop\Pdf\Pdf;
use Pop\Color\Color\Rgb;
$document = new Document(new Page(Page::LETTER));
$document->addFont(Font::ARIAL);
$page = $document->getPage(1);
$note = new Text('Pop PDF counts the wrapped lines before drawing them, so a background box can be sized to fit exactly.', size: 12);
$note->setFillColor(new Rgb(0, 0, 0));
$note->setCharWrap(30, leading: 14);
$height = $note->getNumberOfWrappedLines() * $note->getLeading(); // 4 * 14 = 56
$box = new Path(Path::FILL);
$box->setFillColor(new Rgb(245, 240, 220));
$box->drawRectangle(95, 700 - $height, 260, h: $height + 6);
$page->addPath($box);
$page->addText($note, 'Arial', x: 100, y: 700);
Pdf::writeToFile($document, filename: __DIR__ . '/sized-note.pdf');
Give the text its own fill color, the same as any other Text — a box drawn immediately before it
would otherwise leave its own fill color in force, and the note would render invisibly.
Alignment#
Text\Alignment measures each word in a string against the font in use and starts a new line before
the first word that would cross the right-hand boundary you set. Build one with createLeft(),
createRight() or createCenter() and give it to a Text with setAlignment():
use Pop\Pdf\Document;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Text;
use Pop\Pdf\Document\Page\Text\Alignment;
use Pop\Pdf\Document\Font;
use Pop\Pdf\Pdf;
$paragraph = 'Pop PDF lays out paragraph text by measuring each word against the font in use and '
. 'breaking the line before the first word that would cross the column boundary you set.';
$document = new Document(new Page(Page::LETTER));
$document->addFont(Font::ARIAL);
$page = $document->getPage(1);
$text = new Text($paragraph, size: 12);
$text->setAlignment(Alignment::createLeft(leftX: 50, rightX: 300));
$page->addText($text, 'Arial', x: 50, y: 700);
Pdf::writeToFile($document, filename: __DIR__ . '/alignment.pdf');
Alignment and Wrap escape each line for you as they lay the paragraph out, the same escaping
Escaping covers for addText().
createLeft(leftX: 50, rightX: 300) sets a 250-point column, and that sentence at 12-point Arial
breaks into 4 lines inside it. $y on addText() positions the first line; each line after steps
down by $leading, the third argument, covered below.
$leftX and $rightX both default to 0 when left off — a zero-width column, not the full page —
so always pass the two boundaries explicitly.
$leading itself defaults to 0 and fills in from the text's font size at compile time, the same
behavior Character Wrapping covers for setCharWrap(). Pass a value
explicitly to open the lines up or pull them tighter:
use Pop\Pdf\Document\Page\Text;
use Pop\Pdf\Document\Page\Text\Alignment;
$text = new Text('Pop PDF lays out paragraph text by measuring each word against the font in use.', size: 12);
$text->setAlignment(Alignment::createLeft(leftX: 50, rightX: 300, leading: 20)); // 20-point leading, not 12
Left#
createLeft($leftX, $rightX, $leading = 0) starts every line flush against $leftX, the ordinary
paragraph layout most body text uses.
Right#
createRight($leftX, $rightX, $leading = 0) ends every line flush against $rightX instead, each
line's ragged edge falling on the left:
use Pop\Pdf\Document\Page\Text;
use Pop\Pdf\Document\Page\Text\Alignment;
$paragraph = 'Pop PDF lays out paragraph text by measuring each word against the font in use and '
. 'breaking the line before the first word that would cross the column boundary you set.';
$text = new Text($paragraph, size: 12);
$text->setAlignment(Alignment::createRight(leftX: 50, rightX: 300));
Same paragraph, same column, still 4 lines — alignment changes where a line sits, not where it breaks.
Center#
createCenter($leftX, $rightX, $leading = 0) centers every line between the two boundaries:
use Pop\Pdf\Document\Page\Text;
use Pop\Pdf\Document\Page\Text\Alignment;
$paragraph = 'Pop PDF lays out paragraph text by measuring each word against the font in use and '
. 'breaking the line before the first word that would cross the column boundary you set.';
$text = new Text($paragraph, size: 12);
$text->setAlignment(Alignment::createCenter(leftX: 50, rightX: 300));
The three give the same 4 lines three silhouettes: flush left with a ragged right edge, flush right with a ragged left edge, centered with both ragged.
Wrapping to a Box#
Text\Wrap does everything Text\Alignment does, plus a second, narrower boundary that applies only
while a line falls inside a box — for flowing a paragraph around an image or pull quote. Build one
with Wrap::createLeft() or Wrap::createRight() — each also takes a $leading argument after the
box, defaulting and filling in the same way Character Wrapping describes — and
set the box with setBox() or setBoxCoordinates():
use Pop\Pdf\Document;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Text;
use Pop\Pdf\Document\Page\Text\Wrap;
use Pop\Pdf\Document\Font;
use Pop\Pdf\Pdf;
$paragraph = 'Pop PDF lays out paragraph text by measuring each word against the font in use and '
. 'breaking the line before the first word that would cross the column boundary you set. '
. 'A box sitting inside the column narrows the lines that fall beside it and widens back '
. 'out again below it, wrapping the text around whatever the box represents.';
$document = new Document(new Page(Page::LETTER));
$document->addFont(Font::ARIAL);
$page = $document->getPage(1);
$box = ['left' => 350, 'right' => 550, 'top' => 720, 'bottom' => 620];
$text = new Text($paragraph, size: 12);
$text->setWrap(Wrap::createLeft(leftX: 50, rightX: 550, box: $box));
$page->addText($text, 'Arial', x: 50, y: 730);
Pdf::writeToFile($document, filename: __DIR__ . '/wrap-box.pdf');
// setBoxCoordinates() is the same box, given as four arguments instead of an array:
$wrap = Wrap::createLeft(leftX: 50, rightX: 550);
$wrap->setBoxCoordinates(350, 550, 720, 620);
For createLeft(), a line stops short of the box's left edge once that line's y falls between the
box's top and bottom — the column narrows from the full 50–550 width to 50–350 for those
lines only, then widens back out below bottom. Nothing is clipped or dropped: a word wider than the
narrowed lane still places whole rather than split, so an unusually long word can run past the box's
edge — keep the lane wide enough for the longest word you expect.
createRight() mirrors this from the other side, narrowing against the box's right edge instead —
the box sits on the left of the column instead of the right:
use Pop\Pdf\Document\Page\Text;
use Pop\Pdf\Document\Page\Text\Wrap;
$paragraph = 'Pop PDF lays out paragraph text by measuring each word against the font in use and '
. 'breaking the line before the first word that would cross the column boundary you set.';
$box = ['left' => 50, 'right' => 250, 'top' => 720, 'bottom' => 620];
$text = new Text($paragraph, size: 12);
$text->setWrap(Wrap::createRight(leftX: 50, rightX: 550, box: $box));
A Wrap built with no box — Wrap::createLeft(leftX: 50, rightX: 300), leaving the third argument
off — behaves exactly like Alignment: getBox() reports all four coordinates at 0, the narrowing
condition never fires, and every line uses the full width, the same 4 lines Alignment produces.
Reach for Wrap once there is a real box to give it; until then the two are interchangeable except
for one case — Wrap alone reads setStrings(), flowing several
separate runs on one Text together as a single paragraph:
use Pop\Pdf\Document\Page\Text;
use Pop\Pdf\Document\Page\Text\Wrap;
$text = new Text();
$text->setStrings(['Hello world foo bar', 'baz qux more words here to wrap']);
$text->setWrap(Wrap::createLeft(leftX: 50, rightX: 300));
Alignment measures getString() only, and a Text built purely from setStrings() never sets
that — pairing setStrings() with setAlignment() compiles but renders no text. Give a multi-run
Text a Wrap, and a single-string one either Alignment or Wrap.
See Also#
- Adding Text — sizing, coloring and rotating the
Textobject this page lays out - Text Streams — flowing text across a box tall enough to overflow onto a
second page, which neither
AlignmentnorWrapdoes on its own - Fonts — the font metrics
AlignmentandWrapmeasure every word against