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:
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:
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:
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:
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:
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():
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 leftnull- 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 raisesTypeErroronce the document compiles — call it at least once before the firstaddText() $alignonly has an effect on a stream holding exactly oneaddText()call in total — pass'center'to center that single run between$startXand$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:
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 startgetOrphanStream()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:
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:
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#
- Adding Text — the single positioned string a stream's runs are built from
- Text Layout & Wrapping —
AlignmentandWrap, for text that fits on one page - Pages & Coordinates —
getTextStreams(),hasTextStreams(), and the page's coordinate origin - Drawing & Paths — drawing the background box a stream's measured height sizes