Pop PDF
Build

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:

PHP
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:

PHP
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:

PHP
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:

PHP
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():

PHP
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:

PHP
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.

createRight($leftX, $rightX, $leading = 0) ends every line flush against $rightX instead, each line's ragged edge falling on the left:

PHP
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:

PHP
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():

PHP
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 50550 width to 50350 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:

PHP
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:

PHP
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 Text object this page lays out
  • Text Streams — flowing text across a box tall enough to overflow onto a second page, which neither Alignment nor Wrap does on its own
  • Fonts — the font metrics Alignment and Wrap measure every word against