Drawing & Paths
A Path is a drawing surface you add to a page, and every shape it can draw is a method on it.
Adding a Path to a Page#
Construct a path, draw on it, and hand it to a page with addPath():
use Pop\Pdf\Document;
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Path;
use Pop\Pdf\Pdf;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL);
$path->setFillColor(new Rgb(0, 102, 204));
$path->drawRectangle(x: 100, y: 200, w: 300, h: 150);
$page = new Page(Page::LETTER);
$page->addPath($path);
Pdf::writeToFile(new Document($page), filename: __DIR__ . '/shapes.pdf');
The constructor argument sets the paint style and defaults to Path::STROKE — see Path
Styles for the full set. Coordinates are PDF points measured from the bottom left
corner of the page (see Pages & Coordinates), and every
coordinate on every method takes an int or a float.
addPath() returns the page and every draw* method returns the path, so calls chain either way. A
page holds as many paths as it needs, each with its own style, colors and stroke width:
use Pop\Pdf\Document\Page;
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$filled = new Path(Path::FILL);
$filled->setFillColor(new Rgb(0, 102, 204))->drawRectangle(x: 100, y: 200, w: 100, h: 100);
$outlined = new Path(Path::STROKE);
$outlined->setStrokeColor(new Rgb(0, 0, 0))->setStroke(3)
->drawRectangle(x: 300, y: 200, w: 100, h: 100);
$page = new Page(Page::LETTER);
$page->addPath($filled)->addPath($outlined);
Fill and Stroke#
A path carries one fill color and one stroke color, and both take any Pop\Color implementation —
Rgb, Cmyk or Grayscale:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Cmyk;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL_STROKE);
$path->setFillColor(new Rgb(0, 102, 204));
$path->setStrokeColor(new Cmyk(0, 0, 0, 100));
$path->setStroke(4);
$path->drawRectangle(x: 100, y: 200, w: 300, h: 150);
The style decides which color paints:
Path::FILL— the interior; ink stops exactly on the shape's edgesPath::STROKE— the outline, straddling the edge so a 4-point stroke puts ink two points beyond every sidePath::FILL_STROKE— both, interior first
Both colors persist across shapes — set one once and reuse it, or change it partway through:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL);
$path->setFillColor(new Rgb(0, 102, 204));
$path->drawSquare(x: 100, y: 600, w: 80);
$path->drawSquare(x: 220, y: 600, w: 80);
$path->setFillColor(new Rgb(204, 102, 0));
$path->drawSquare(x: 340, y: 600, w: 80);
setStroke() takes a width and, optionally, a dash length and a dash gap:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path();
$path->setStrokeColor(new Rgb(0, 0, 0));
$path->setStroke(2, dashLength: 8, dashGap: 4);
$path->drawLine(x1: 100, y1: 400, x2: 400, y2: 400);
A dash pattern needs both $dashLength and $dashGap; passing only one, or neither, draws a solid
line with no error.
setStroke(0) is not "no stroke" — it still emits the 0 w operator, the thinnest line the device
renders (one pixel at 72 DPI, finer as resolution rises). For no outline at all, construct with
Path::FILL instead.
Shapes#
Where a method takes a $w and an optional $h, leaving $h off makes the shape square or circular:
drawRectangle(x: 100, y: 200, w: 150) draws a 150-point square and
drawEllipse(x: 300, y: 400, w: 100) a circle.
Lines#
drawLine($x1, $y1, $x2, $y2) runs a straight line between two points:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path();
$path->setStrokeColor(new Rgb(0, 0, 0));
$path->setStroke(1);
$path->drawLine(x1: 100, y1: 400, x2: 400, y2: 400);
A line always strokes, regardless of the path's style, taking its color from setStrokeColor() and
width/dash from setStroke().
Rectangles and Squares#
drawRectangle($x, $y, $w, $h = null) places its lower left corner at $x, $y and extends right and
up. drawSquare($x, $y, $w) is the same call with one dimension:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL);
$path->setFillColor(new Rgb(0, 102, 204));
$path->drawRectangle(x: 100, y: 550, w: 300, h: 150);
$path->drawSquare(x: 100, y: 350, w: 150);
Rounded Rectangles and Squares#
drawRoundedRectangle($x, $y, $w, $h = null, $rx = 10, $ry = null) and
drawRoundedSquare($x, $y, $w, $rx = 10, $ry = null) take the same origin and dimensions plus two
corner radii. $rx is the horizontal radius and $ry the vertical one; leave $ry off and it
matches $rx:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL);
$path->setFillColor(new Rgb(0, 102, 204));
$path->drawRoundedRectangle(x: 100, y: 200, w: 300, h: 150, rx: 40, ry: 20);
The radii carve into the corners; the bounding box itself is unchanged.
Polygons#
drawPolygon() takes an array of points, each with an x and y key, closes the outline back to the
first point, and paints itself in the path's style — as of 6.0.7; earlier versions emitted no paint
operator at all:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL_STROKE);
$path->setFillColor(new Rgb(230, 230, 230));
$path->setStrokeColor(new Rgb(0, 0, 0));
$path->setStroke(2);
$path->drawPolygon(points: [
['x' => 100, 'y' => 400],
['x' => 250, 'y' => 520],
['x' => 400, 'y' => 400],
]);
Every point needs both keys — one missing either raises Pop\Pdf\Document\Page\Exception.
Ellipses and Circles#
drawEllipse($x, $y, $w, $h = null) and drawCircle($x, $y, $w) are centered on $x, $y, and $w
and $h are the horizontal and vertical radii rather than the full width and height:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL);
$path->setFillColor(new Rgb(0, 102, 204));
$path->drawEllipse(x: 300, y: 600, w: 150, h: 75);
$path->drawCircle(x: 300, y: 400, w: 100);
Arcs, Chords and Pies#
drawArc(), drawChord() and drawPie() share one signature — ($x, $y, $start, $end, $w, $h = null) — with $x, $y the center and $w/$h the radii, as with an ellipse. Angles are degrees
counterclockwise from 3 o'clock, so 0, 90 sweeps the upper right quadrant:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL);
$path->setFillColor(new Rgb(0, 102, 204));
$path->drawPie(x: 200, y: 600, start: 0, end: 90, w: 120);
$path->drawChord(x: 450, y: 600, start: 0, end: 90, w: 120);
$path->setStyle(Path::STROKE);
$path->setStrokeColor(new Rgb(0, 0, 0));
$path->setStroke(3);
$path->drawArc(x: 300, y: 300, start: 0, end: 270, w: 120);
The three differ only in how the sweep closes:
- arc — the curve alone, usually stroked
- chord — closed with a straight line between the end points, filling the segment above it
- pie — closed through the center, filling the whole wedge
Start must be 0 or more, end 360 or less, and start less than end — any other pair raises
OutOfRangeException naming the rule broken. Sweeps over 90 degrees split into several curve segments
internally, so 0, 270 draws as one continuous arc.
Bézier Curves#
Cubic#
drawOpenCubicBezierCurve($x1, $y1, $x2, $y2, $bezierX1, $bezierY1, $bezierX2, $bezierY2) takes the
two end points first and the two control points after them:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path();
$path->setStrokeColor(new Rgb(0, 0, 0));
$path->setStroke(3);
$path->drawOpenCubicBezierCurve(
x1: 100, y1: 300, x2: 400, y2: 300,
bezierX1: 175, bezierY1: 550, bezierX2: 325, bezierY2: 550
);
drawClosedCubicBezierCurve() takes the identical eight arguments and adds the straight segment back
to the start point, so a stroke style outlines that segment too.
Quadratic#
drawOpenQuadraticBezierCurve($x1, $y1, $x2, $y2, $bezierX, $bezierY, $first = true) steers with a
single control point, and $first decides which end that point is anchored to:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path();
$path->setStrokeColor(new Rgb(0, 0, 0));
$path->setStroke(2);
$path->drawOpenQuadraticBezierCurve(
x1: 100, y1: 300, x2: 500, y2: 300, bezierX: 150, bezierY: 600
);
$path->drawOpenQuadraticBezierCurve(
x1: 100, y1: 200, x2: 500, y2: 200, bezierX: 150, bezierY: 500, first: false
);
true, the default, anchors the control point to the curve's start; false anchors it to the end.
drawClosedQuadraticBezierCurve() takes the same seven arguments and closes back to the start,
outlined the same way the closed cubic form is.
Layers#
openLayer() and closeLayer() save and restore the graphics state around a run of drawing — colors,
stroke settings and clipping applied inside are discarded when it closes, and whatever was in force
before returns:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL);
$path->setFillColor(new Rgb(0, 0, 0));
$path->openLayer()
->setFillColor(new Rgb(200, 0, 0))
->drawSquare(x: 100, y: 500, w: 120)
->closeLayer();
$path->drawSquare(x: 300, y: 500, w: 120);
The red set inside the layer lasts until closeLayer(); the black set before the layer is what the
second square picks up once it closes.
Layers nest, and each closeLayer() restores the state its matching openLayer() saved:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL);
$path->setFillColor(new Rgb(0, 0, 0));
$path->openLayer()
->setFillColor(new Rgb(200, 0, 0))
->openLayer()
->setFillColor(new Rgb(0, 150, 0))
->drawSquare(x: 100, y: 400, w: 80)
->closeLayer()
->drawSquare(x: 220, y: 400, w: 80)
->closeLayer();
$path->drawSquare(x: 340, y: 400, w: 80);
Innermost first, those three squares come out green, red and black.
A path keeps its drawing as a list of content stream fragments, and getStreams() hands that list
back. Opening a layer appends the PDF graphics state save operator and closing one appends the
restore:
use Pop\Pdf\Document\Page\Path;
$path = new Path();
$path->openLayer()->drawLine(x1: 100, y1: 400, x2: 400, y2: 400)->closeLayer();
foreach ($path->getStreams() as $stream) {
echo trim($stream['stream']) . "\n";
}
That prints q, the line's two point placeholders, its S operator, then Q. The placeholders hold
coordinates unresolved until the document compiles, against whatever origin is in force then.
Path Styles#
The style is what the path does with each finished shape. setStyle() sets it, and every shape drawn
after the call uses the new value, so one path can fill one shape and outline the next:
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL);
$path->setFillColor(new Rgb(0, 102, 204));
$path->setStrokeColor(new Rgb(0, 0, 0));
$path->setStroke(3);
$path->drawRectangle(x: 100, y: 200, w: 100, h: 100);
$path->setStyle(Path::STROKE);
$path->drawRectangle(x: 300, y: 200, w: 100, h: 100);
The first rectangle is solid blue; the second, a black outline. Pass the constants themselves rather
than their string values — setStyle() keeps the style the path already has for any other value, and
getStyle() reports the one currently in force.
| Constant | Paints |
|---|---|
Path::STROKE |
the outline |
Path::STROKE_CLOSE |
the outline, with the shape closed back to its start point first |
Path::FILL |
the interior |
Path::FILL_EVEN_ODD |
the interior, under the even-odd rule |
Path::FILL_STROKE |
the interior, then the outline over it |
Path::FILL_STROKE_EVEN_ODD |
both, filling under the even-odd rule |
Path::FILL_STROKE_CLOSE |
both, with the shape closed first |
Path::FILL_STROKE_CLOSE_EVEN_ODD |
both, closed first and filled under the even-odd rule |
Path::CLIPPING |
nothing, and adds the shape to the clipping region, leaving the path open for the next shape to join |
Path::CLIPPING_FILL |
the interior, and clips to the shape |
Path::CLIPPING_NO_STYLE |
nothing, and clips under the nonzero winding rule, ending the path there |
Path::CLIPPING_EVEN_ODD |
nothing, and clips under the even-odd rule, leaving the path open for the next shape to join |
Path::CLIPPING_EVEN_ODD_FILL |
the interior, and clips under the even-odd rule |
Path::CLIPPING_EVEN_ODD_NO_STYLE |
nothing, and clips under the even-odd rule, ending the path there |
Path::NO_STYLE |
nothing — the shape contributes its geometry and no ink |
Even-Odd Filling#
The plain fill styles use the nonzero winding rule; the EVEN_ODD variants use the even-odd rule.
They agree on a single shape and diverge only when one outline sits inside another on the same path —
that's how a hole gets cut. Since every shape paints itself as soon as it's drawn, cutting a hole
needs openPath() and paintPath() to hold several outlines on one path and paint them together:
shapes drawn between the two calls contribute outlines without painting, and paintPath() emits the
style once for all of them.
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::FILL_EVEN_ODD);
$path->setFillColor(new Rgb(0, 102, 204));
$path->openPath();
$path->drawPolygon(points: [
['x' => 150, 'y' => 250], ['x' => 450, 'y' => 250],
['x' => 450, 'y' => 550], ['x' => 150, 'y' => 550],
]);
$path->drawPolygon(points: [
['x' => 250, 'y' => 350], ['x' => 350, 'y' => 350],
['x' => 350, 'y' => 450], ['x' => 250, 'y' => 450],
]);
$path->paintPath();
That fills a 300-point square with a blank 100-point hole — an 80,000-point ring. Swap in
Path::FILL and the same two outlines fill solid, all 90,000 points. Skip openPath() and the two
drawPolygon() calls each paint themselves as drawn — one solid square stacked on another, since
even-odd only cuts a hole when it reads both outlines from a single path.
Clipping#
A clipping style turns a shape into a mask for everything drawn after it on the same path. Set
Path::CLIPPING_NO_STYLE — usable as of 6.0.7, silently ignored as an unrecognized style before it —
or Path::CLIPPING_EVEN_ODD_NO_STYLE, draw the mask, switch to a paint style, and draw the content
inside a layer so the mask lifts at closeLayer():
use Pop\Pdf\Document\Page\Path;
use Pop\Color\Color\Rgb;
$path = new Path(Path::CLIPPING_EVEN_ODD_NO_STYLE);
$path->openLayer()->drawCircle(x: 300, y: 400, w: 100);
$path->setStyle(Path::FILL)
->setFillColor(new Rgb(0, 102, 204))
->drawRectangle(x: 100, y: 200, w: 400, h: 400);
$path->closeLayer();
The 400-point square comes out as a 200-point blue disc — only the part inside the circle reaches the
page. Path::CLIPPING_NO_STYLE reads the mask under the nonzero winding rule and
Path::CLIPPING_EVEN_ODD_NO_STYLE under the even-odd rule; a single circle gives them nothing to
disagree about, so both produce the same disc here. The choice matters only once the mask combines
more than one outline, as in Even-Odd Filling. Path::CLIPPING_FILL and
Path::CLIPPING_EVEN_ODD_FILL clip the same way and also paint the mask shape in the fill color, so
the circle stays visible under whatever the clip lets through.
See Also#
- Pages & Coordinates — page sizes and the origin every coordinate here is measured from
- Images — placing raster artwork alongside drawn shapes
- Creating Documents — assembling pages into a document and writing it out