Skip to content

Creating PDFs with PHP, part 3: Drawing

Now that we can create blank PDF's, it's time to add some stuff. Vector drawing commands (lines and shapes) are simple; you just add the commands to the page content stream. In terms of the original class that would be:

$this->pages[count($this->pages)-1]->contents .= "the command\n";
// we just need some whitespace at the end, but the newline makes it easier to read the resulting PDF

But to make things easier, we can keep track of the last page:

function newpage(){
  parent::newpage();
  $this->currentPage = $this->pages[count($this->pages)-1];
}
// and now adding commands is:
$this->currentPage->contents .= "the command\n";
// this also has the advantage that we can manipulate currentPage to add commands to other content streams

There are lots of commands, all of which are postfix (parameters come before operators). There are no math operators or stack manipulation operators; any calculation has to be done before generating the PDF and numbers inserted directly.

See the code.

Drawing is done by creating a path, then filling or stroking (outlining) it. Some useful commands include:

x y m
Move to point (x, y).
x y l
Draw a straight line from the current point to (x, y).
h
Close the current path by drawing a line back to the last point moved to. You have to explicitly close a path to fill it, and even stroking it looks better if it is closed since PDF will make the last line join the first with an elegant miter. Just drawing a line to the starting point (like 0 0 m 1 0 l 0 1 l 0 0 l) doesn't count.
x1 y1 x2 y2 x3 y3 c
Draw a cubic Bézier curve from the current point to (x3, y3), with (x1, y1) and (x2, y2) as control points. You can play with Bézier curves and manipulate control points to get a feel for them at Mike Kamermans's site.
x y w h re
Draw a closed rectangle from (x, y) with width w and height h. Shorthand for x y m (x + w) y l (x + w) (y + h) l x (y + h) l h. But PDF will do the math for you.
r g b RG
Set the stroking color to (r, g, b) where the color components are fractional, from 0 to 1, unlike PHP where the components go from 0 to 255.
r g b rg
Set the fill color to (r, g, b).
w w
Set the line width for stroking to w.
S
Stroke the current path. The fill and stroke commands discard the current path. That means you can't fill then fill a path; you have to recreate it.
f
Fill the current path.

So extending the basic class to allow drawing is straightforward. The only gotcha is that the coordinate system is backwards: the origin is at the lower left corner and the y axis goes up (like a mathematical graph), unlike PHP imaging and every other computer graphics package I've ever used, where the origin is at the top left.

See a sample page.

How'd you do that cool Apple logo?

Luckily, SVG uses Bézier curves as well, and can be easily read (paths like M 1,2 C 3,4 5,6 7,8 in SVG becomes 1 2 m 3 4 5 6 7 8 c in PDF). You just have to invert the y-values to reflect the reflected coordinate system. So finding a good SVG image lets you create a PDF image:

$pdf->moveto(28.70919,92.37034);
$pdf->curveto(32.22477,92.37025,36.46696,91.64368,41.43575,90.19065);
$pdf->curveto(46.45132,88.73743999999999,49.77944,88.01088,51.42013,88.01096);
$pdf->curveto(53.52944,88.01088,56.97475,88.83118999999999,61.756057,90.4719);
$pdf->curveto(66.537237,92.11243,70.685667,92.93275,74.201377,92.93284);
$pdf->curveto(79.966907,92.93275,85.099717,91.38587,89.599807,88.29221);
$pdf->curveto(92.130957,86.51088,94.638767,84.09682000000001,97.123244,81.05002);
$pdf->curveto(93.373147,77.862449,90.630967,75.02649,88.896687,72.54219);
$pdf->curveto(85.755967,68.04213,84.185657,63.07338,84.185747,57.63594);
$pdf->curveto(84.185657,51.682770000000005,85.849717,46.31558,89.177937,41.53438);
$pdf->curveto(92.505957,36.75309,96.302824,33.729659999999996,100.56855,32.46407000000001);
$pdf->curveto(98.787204,26.69842,95.834084,20.674989999999994,91.709187,14.393749999999997);
$pdf->curveto(85.474717,4.971879999999999,79.287227,0.26094000000000506,73.146687,0.26094000000000506);
$pdf->curveto(70.709107,0.26094000000000506,67.334107,1.0343799999999987,63.021687,2.581249999999997);
$pdf->curveto(58.755997,4.128129999999999,55.14663,4.9015600000000035,52.19356,4.901570000000007);
$pdf->curveto(49.24038,4.9015600000000035,45.79507,4.104690000000005,41.85763,2.510940000000005);
$pdf->curveto(37.96696,0.8703200000000066,34.8029,0.05001000000000033,32.36544,0.04999999999999716);
$pdf->curveto(25.00603,0.05001000000000033,17.78729,6.284369999999996,10.70919,18.75313);
$pdf->curveto(3.63105,31.081220000000002,0.09199,43.17496,0.092,55.03438);
$pdf->curveto(0.09199,66.04993999999999,2.7873,75.02649,8.1779402,81.96409);
$pdf->curveto(13.61542,88.9015,20.45916,92.37025,28.70919,92.37034);
$pdf->moveto(73.006057,120.07346);
$pdf->curveto(73.193477,119.46397,73.310667,118.92491,73.357627,118.45627);
$pdf->curveto(73.404417,117.98741,73.427857,117.51866,73.427937,117.05002);
$pdf->curveto(73.427857,114.04991,72.724727,110.76867,71.318557,107.20627);
$pdf->curveto(69.912237,103.64367,67.685677,100.33899,64.638877,97.29221);
$pdf->curveto(62.013807,94.71399,59.412247,92.97962,56.83419,92.08909);
$pdf->curveto(55.1935,91.57337,52.70913,91.17493,49.38106,90.89377);
$pdf->curveto(49.47476,98.01868,51.32632,104.18272999999999,54.93575,109.38596);
$pdf->curveto(58.591927,114.58897,64.615367,118.15147,73.006057,120.07346);
$pdf->fill (0,149,182); // biondi blue

Yes, the image is trademarked. But I think this use is OK, since I'm acknowledging it as the Apple logo. If Apple's lawyer's find me, I'll be thrilled to be so important.

The code for the rectangle and S-curve is:

// filled rectangle
$pdf->moveto(100, 100);
$pdf->lineto(100,200);
$pdf->lineto(200,200);
$pdf->lineto(200,100);
$pdf->closepath();
$pdf->fill(0,255,0);
// rectangle outline, using the re command
$pdf->rect(100, 100, 100, 100);
$pdf->linewidth(10);
$pdf->stroke(255, 0, 0);
// Bézier curve
$pdf->moveto(200, 250);
$pdf->curveto(100, 200, 300, 200, 200, 150);
$pdf->stroke(0, 0, 0);

I don't need no Bézier curves—I need circles!

Bézier curves are nice mathematically; they're polynomials, easily differentiated and easy to splice together in esthetically pleasing ways. But they're not circles. And you can't get an exact circle with cubic Béziers. The best you can do is approximate a set of arcs and join them together. To get a good approximation, each arc has to be 90 degrees or less. You match the slopes at the endpoints and the curvature at the midpoint, and you can do very well: according to Don Lancaster the worst error is one part in a thousand. If you need better than that, you shouldn't be getting your drafting advice from a pediatrician. The math for an arbitrary arc of an arbitrarily oriented ellipse is straightforward but hairy; L. Maisonobe has the details. Since I'm so nice, I implemented it in PHP:

define ('M_TAU', 2*M_PI); // http://www.math.utah.edu/~palais/pi.html
function ellipsearc ($x, $y, $a, $b=NULL, $theta=0, $lambda1=0, $lambda2=0){
	// get the Bézier points of the elliptical arc with the ellipse centered at $x, $y, with major and minor semi-axes $a and $b,
	// and major axis at $theta to the x-axis,
	// with the arc starting at angle $lambda1 from the major axis (not the x-axis!) and ending at $lambda2
	// all angles are in radians.
	// returns an array of arrays, with each sub-array being a segment
	// of the final curve, with 8 elements, the x and y coordinates of
	// the control points.

	// normalize parameters
	if ($b === NULL) $b = $a;
	while ($lambda1 < 0) $lambda1 += M_TAU;
	while ($lambda1 >= M_TAU)  $lambda1 -= M_TAU;
	while ($lambda2 <= 0) $lambda2 += M_TAU;
	while ($lambda2 > M_TAU)  $lambda2 -= M_TAU;
	if ($lambda2 < $lambda1){
		// goes through 0; split at 0;
		return array_merge(
			ellipsearc($x, $y, $a, $b, $theta, $lambda1, M_TAU),
			ellipsearc($x, $y, $a, $b, $theta, 0, $lambda2)
		);
	}else if ($lambda2 - $lambda1 > M_PI_2){
		// Draw arcs less than 90 degrees, so bisect the arc
		return array_merge(
			ellipsearc($x, $y, $a, $b, $theta, $lambda1, ($lambda2+$lambda1)/2),
			ellipsearc($x, $y, $a, $b, $theta, ($lambda2+$lambda1)/2, $lambda2)
		);
	}
	
	// scale angles.
	$eta1 = scaleangle($lambda1, $a, $b);
	$eta2 = scaleangle($lambda2, $a, $b);
	
	// find the control points
	$x0 = ellipsepoint($x, $y, $a, $b, $theta, $eta1);
	$x3 = ellipsepoint($x, $y, $a, $b, $theta, $eta2);
	$delta = $eta2-$eta1;
	$tan = tan($delta/2);
	$alpha = sin($delta)*(sqrt(4+3*$tan*$tan)-1)/3; // Maisonobe's equation 15
	// formulae from Maisonobe's section 3.4.1
	$x1 = controlpoint($x0, $alpha);
	$x2 = controlpoint($x3, -$alpha);
	return array(array(
		$x0[0], $x0[1],
		$x1[0], $x1[1],
		$x2[0], $x2[1],
		$x3[0], $x3[1]
	));
}
function scaleangle($lambda, $a, $b){
	// parameterized angle from Maisonobe's section 2.2.1
	return atan2(sin($lambda)/$b, cos($lambda)/$a);
}
function ellipsepoint($x, $y, $a, $b, $theta, $eta){
	// parameterization of an ellipse. Maisonobe's equation 3 and 4
	// returns a 4-element array, with the coordinates first then the derivative
	$costheta = cos($theta);
	$sintheta = sin($theta);
	$coseta = cos($eta);
	$sineta = sin($eta);
	return array(
		$x+$a*$costheta*$coseta-$b*$sintheta*$sineta,
		$y+$a*$sintheta*$coseta+$b*$costheta*$sineta,
		  -$a*$costheta*$sineta-$b*$sintheta*$coseta,
		  -$a*$sintheta*$sineta+$b*$costheta*$coseta,
	);
}
function controlpoint($x, $alpha){
	// returns a point (as a 2-element array) that is $alpha from $x [0,1] along the vector $x[2,3]
	return array(
		$x[0]+$alpha*$x[2],
		$x[1]+$alpha*$x[3]	
	);
}

For example, ellipsearc (100, 120, 45) returns the curves for a circle centered at (100,120) with radius 45, ellipsearc (100, 120, 50, 40) gives an ellipse centered at (100,120) with major axis horizontal, with major semiaxis (half the width) 50 and minor semiaxis (half the height) 40. The "major axis" can be shorter than the minor axis; the designation is just of the axis that determines the orientation. ellipsearc (100, 120, 50, 40, deg2rad(60)) gives an ellipse centered at (100,120) with major axis at 60 degrees from the horizontal, with major semiaxis (half the width) 50 and minor semiaxis (half the height) 40. ellipsearc (100, 120, 45, NULL, 0, deg2rad(45), deg2rad(135)) returns the curves for an arc of a circle centered at (100,120) with radius 45, with the arc extending from 45 degrees to 135 degrees.

It returns an array of arrays, like:

[
  [10,10,20,20,30,30,40,40], // a Bézier curve from (10,10) to (40,40) with control points (20,20) and (30, 30)
  [40,40,50,50,60,60,70,70],
  [80,80,90,90,100,100,10,10]
]

So to draw the ellipse from the sample, use

$arcs = ellipsearc (200, 600, 150, 50);
$pdf->moveto($arc[0][0], $arc[0][1]);
foreach ($arcs as $arc) $pdf->curveto($arc[2], $arc[3], $arc[4], $arc[5], $arc[6], $arc[7]);
$pdf->closepath();
$pdf->stroke (0, 127, 127);

And to draw a wedge (e.g. pie chart):

$arcs = ellipsearc (200, 600, 150, 150, 0, 0, deg2rad(60));
$pdf->moveto(200,600); // the center
$pdf->lineto($arc[0][0], $arc[0][1]);
foreach ($arcs as $arc) $pdf->curveto($arc[2], $arc[3], $arc[4], $arc[5], $arc[6], $arc[7]);
$pdf->closepath();
$pdf->fill(0, 127, 127);

Hope this helps someone; it was interesting for me.

Continued…

Post a Comment

Your email is never published nor shared. Required fields are marked *