Pop PDF
Build

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

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

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

PHP
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 edges
  • Path::STROKE — the outline, straddling the edge so a 4-point stroke puts ink two points beyond every side
  • Path::FILL_STROKE — both, interior first

Both colors persist across shapes — set one once and reuse it, or change it partway through:

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

PHP
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