Pop PDF
Build

Text Streams

A Text\Stream is a block of running text — several styled runs flowing into one another, wrapping at a right edge and, unlike Alignment or Wrap, able to tell you when it has run out of room on the page.

Creating a Text Stream#

The constructor takes three required coordinates and a fourth, optional one:

PHP
use Pop\Pdf\Document\Page\Text\Stream;

$stream = new Stream(startX: 60, startY: 700, edgeY: 550, edgeY: 60);

$startX, $startY is where the first line begins. $edgeX is the right boundary a line wraps before crossing, the same role Wrap's right-hand boundary plays. $edgeY, left off above, is the bottom boundary; a stream built without one, new Text\Stream(60, 700, 550), has no bottom edge at all — Orphans covers what that does to overflow detection.

getStartX(), getStartY(), getEdgeX() and getEdgeY() read all four back, each with a matching setter:

PHP
use Pop\Pdf\Document\Page\Text\Stream;

$stream = new Stream(startX: 60, startY: 700, edgeX: 550, edgeY: 60);
echo $stream->getStartX();  // 60
echo $stream->getEdgeY();   // 60

$stream->setEdgeY(90); // move the bottom margin up 30 points

A stream also tracks a current x and y, separate from where it starts — getCurrentX()/getCurrentY() read them, setCurrentX()/setCurrentY() write them, and both start at null:

PHP
use Pop\Pdf\Document\Page\Text\Stream;

$stream = new Stream(startX: 60, startY: 700, edgeX: 550, edgeY: 60);
var_export($stream->getCurrentX()); // NULL

The first time the stream is laid out, null is treated as "start from $startX, $startY" — the distinction that makes it possible to resume a stream partway through instead of always drawing from its own top. Orphans sets the cursor explicitly to flow overflow onto a fresh page.

Adding Text to a Stream#

addText($string, $y = null, $newLine = false) appends one run. setCurrentStyle($font, $size, $color = null, $align = null) sets the font, size, color and alignment for the next run added:

PHP
use Pop\Pdf\Document\Page\Text\Stream;
use Pop\Color\Color\Rgb;

$stream = new Stream(startX: 60, startY: 700, edgeX: 550, edgeY: 60);
$stream->setCurrentStyle('Arial', 12, color: new Rgb(0, 0, 0));
$stream->addText('Pop PDF flows this paragraph across the width of the stream, wrapping it at the right edge and stepping down a line at a time.');

Call setCurrentStyle() before the addText() call it should style — a style applies to the run about to be added, not runs already there, so styling the second of two runs differently means calling setCurrentStyle() again between them:

PHP
use Pop\Pdf\Document\Page\Text\Stream;
use Pop\Color\Color\Rgb;

$stream = new Stream(startX: 60, startY: 700, edgeX: 550, edgeY: 60);

$stream->setCurrentStyle('Arial', 12, color: new Rgb(0, 0, 0));
$stream->addText('Regular text, then ');
$stream->setCurrentStyle('Arial', 14, color: new Rgb(200, 0, 0));
$stream->addText('a larger, red run.', y: 16, newLine: true);

A style set once carries forward to every run added after it, so a stream that never changes style mid-way needs only one setCurrentStyle() call, before the first addText():

PHP
use Pop\Pdf\Document\Page\Text\Stream;
use Pop\Color\Color\Rgb;

$stream = new Stream(startX: 60, startY: 700, edgeY: 550, edgeY: 60);
$stream->setCurrentStyle('Arial', 12, color: new Rgb(0, 0, 0));
$stream->addText('First run, ');
$stream->addText('second run, ');
$stream->addText('and a third — all three still Arial, 12, black.');
  • addText()'s second argument, the line height, steps down when this run starts a new line — the first run after an explicit $newLine = true, or a wrapped line that crossed $edgeX — defaulting to the run's own font size when left null
  • leaving the color argument off doesn't fall back to black: no color operator is written for a run with no color set, so it renders in whatever fill color the page already has in force from whatever was drawn immediately before it — give every stream its own color explicitly
  • a stream that never gets a setCurrentStyle() call at all raises TypeError once the document compiles — call it at least once before the first addText()
  • $align only has an effect on a stream holding exactly one addText() call in total — pass 'center' to center that single run between $startX and $edgeX

Measuring Before Drawing#

measureHeight($fonts) lays the stream's runs out against the fonts you give it and returns the total rendered height, without drawing anything or touching $edgeY — the way to size a background box before committing the block to a page:

PHP
use Pop\Pdf\Document;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Text\Stream;
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);

$stream = new Stream(startX: 60, startY: 700, edgeX: 300);
$stream->setCurrentStyle('Arial', 12, color: new Rgb(0, 0, 0));
$stream->addText('Pop PDF measures a text stream before it ever touches a page, so you can size a background box to fit the content exactly.');

$fonts  = ['Arial' => new Font('Arial')];
$height = $stream->measureHeight($fonts); // 36 - three 12-point lines

$box = new Path(Path::FILL);
$box->setFillColor(new Rgb(230, 240, 250));
$box->drawRectangle(55, 700 - $height, 250, h: $height + 4);
$page->addPath($box);
$page->addTextStream($stream);

Pdf::writeToFile($document, filename: __DIR__ . '/measured-box.pdf');

$fonts is an array of Document\Font objects keyed by the same name setCurrentStyle() was given — Page::addTextStream() never needs it directly, since the document supplies it at compile time, but measureHeight() and hasOrphans() both need it up front.

Orphans#

$edgeY is what turns a stream into something that can overflow. hasOrphans($fonts) lays the stream out the same way measureHeight() does, but stops the moment a line would cross $edgeY, and returns whether it had to. getOrphanStream() then hands back everything from that stopping point on, ready to flow onto the next page.

Both methods mutate the object they are called on:

  • hasOrphans() leaves its cursor at the break point rather than back at the start
  • getOrphanStream() trims the stream's own runs down to the leftover ones and returns the same object, not a copy
  • calling either on a stream already added to a page corrupts that page

Run the check on a throwaway clone instead, so the original is never touched and still renders its own content, up to where $edgeY cuts it off, exactly as built:

PHP
use Pop\Pdf\Document;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Text\Stream;
use Pop\Pdf\Document\Font;
use Pop\Pdf\Pdf;
use Pop\Color\Color\Rgb;

$words = [];
for ($i = 1; $i <= 900; $i++) {
    $words[] = "word{$i}";
}

$stream = new Stream(startX: 60, startY: 700, edgeX: 550, edgeY: 60);
$stream->setCurrentStyle('Arial', 12, color: new Rgb(0, 0, 0));
$stream->addText(implode(' ', $words));

$document = new Document();
$document->addFont(Font::ARIAL);

$page1 = new Page(Page::LETTER);
$page2 = new Page(Page::LETTER);

$fonts = ['Arial' => new Font('Arial')];
$probe = clone $stream;

if ($probe->hasOrphans($fonts)) {
    $page1->addTextStream($stream);

    $orphan = $probe->getOrphanStream();
    $orphan->setCurrentX(60)->setCurrentY(700); // resume at the top of page 2
    $page2->addTextStream($orphan);
}

$document->addPage($page1);
$document->addPage($page2);

Pdf::writeToFile($document, filename: __DIR__ . '/flowing-text.pdf');

That 900-word block, wrapped at a 490-point line width with a 60-point bottom margin, breaks after word505 — page 1 renders word1 through word505 and page 2 picks up at word506 and runs to word900, every word accounted for exactly once.

getOrphanStream() resets the runs it hands back but not the cursor — $orphan's currentX/currentY are still sitting at the break point from hasOrphans()'s walk down page 1, so page 2 needs them set back to its own top before it draws, the way the fourth and fifth lines above do. hasOrphanIndex() reports whether the last hasOrphans() call found a break, without re-running the layout — useful for checking the answer again, such as deciding whether to allocate a second page at all.

Call getOrphanStream() only after hasOrphans() has returned true on that same stream — on one hasOrphans() was never run against, or found no break in, it reads past the empty break point it expects and is not a reliable way to get an empty stream back.

Multi-Column Layouts#

Two Stream objects with different $startX/$edgeX ranges, both added to the same page, lay out as independent columns. Give the left one an $edgeY at the bottom of its column and run the same orphan technique against it, and the overflow that would otherwise carry onto a second page carries into the second column instead:

PHP
use Pop\Pdf\Document;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Text\Stream;
use Pop\Pdf\Document\Font;
use Pop\Pdf\Pdf;
use Pop\Color\Color\Rgb;

$words = [];
for ($i = 1; $i <= 270; $i++) {
    $words[] = "word{$i}";
}

$left = new Stream(startX: 60, startY: 700, edgeX: 290, edgeY: 400);
$left->setCurrentStyle('Arial', 11, color: new Rgb(0, 0, 0));
$left->addText(implode(' ', $words));

$fonts = ['Arial' => new Font('Arial')];
$probe = clone $left;

$document = new Document(new Page(Page::LETTER));
$document->addFont(Font::ARIAL);
$page = $document->getPage(1);

if ($probe->hasOrphans($fonts)) {
    $page->addTextStream($left);

    $right = $probe->getOrphanStream();
    $right->setStartX(320)->setEdgeX(550);   // the right column's own x range
    $right->setCurrentX(320)->setCurrentY(700);
    $page->addTextStream($right);
}

Pdf::writeToFile($document, filename: __DIR__ . '/columns.pdf');

That 270-word block breaks after word143 — the left column holds word1 through word143 and the right one picks up at word144 and finishes at word270, both sharing the same 400-point bottom margin.

getOrphanStream() carries over the left stream's $startX and $edgeX along with its leftover runs, so the right column needs both moved to its own x range before it draws — left as is, the second half would render on top of the first, in the first column. If the right column can itself overflow past its own $edgeY, check it with hasOrphans() the same way and carry the remainder onto a third column or a new page.

See Also#