PDF::Bu...:Content(3)

✨ NAME ✨

okay bestie, meet PDF::Builder::Content(3) β€” Methods for adding graphics and text to a PDF. This is the User Contributed Perl Documentation page.

Inherits from PDF::Builder::Basic::PDF::Dict.

🌸 SYNOPSIS 🌸

Start with a PDF page (new or opened):

# Start with a PDF page (new or opened)
my $pdf = PDF::Builder->new();
my $page = $pdf->page();

# Add new content object(s)
my $content = $page->graphics();  # or gfx()
#   and/or (as separate object name)
my $content = $page->text();

# Then call the methods below to add graphics and text to the page.
# Note that negative coordinates can have unpredictable effects, so
# keep your coordinates non-negative!

These methods add content to streams output for text or graphics objects. Unless otherwise restricted by a check that we are in or out of text mode, many methods listed here apply equally to text and graphics streams. It is possible that there are some which have no effect in one stream type or the other, but are currently lacking a check to prevent them from being inserted into an inapplicable stream.

πŸ’… METHODS πŸ’…

All public methods listed, except as otherwise noted, return $self, for ease of chaining calls. periodt.

πŸ¦‹ Coordinate Transformations πŸ¦‹

The methods in this section change the coordinate system for the current content object relative to the rest of the document. Note: the changes are relative to the original page coordinates (and thus, absolute), not to the previous position! Thus, "translate(10, 10); translate(10, 10);" ends up only moving the origin to "[10, 10]", rather than to "[20, 20]". There is one call, transform_rel(), which makes your changes relative to the previous position.

If you call more than one of these methods, the PDF specification recommends calling them in the following order: translate, rotate, scale, skew. Each change builds on the last, and you can get unexpected results when calling them in a different order.

CAUTION: a text object ($content) behaves a bit differently. Individual translate, rotate, scale, and skew calls cancel out any previous settings. If you want to combine multiple transformations for text, use the "transform" call.

translate

$content->translate($dx,$dy)

Moves the origin along the x and y axes to $dx and $dy respectively.

rotate

$content->rotate($degrees)

Rotates the coordinate system counter-clockwise (anti-clockwise) around the current origin. Use a negative argument to rotate clockwise. Note that 360 degrees will be treated as 0 degrees.

Note: Unless you have already moved (translated) the origin, it is, and will remain, at the lower left corner of the visible sheet. It will not automatically shift to another corner. For example, a rotation of +90 degrees (counter-clockwise) will leave the entire visible sheet in negative Y territory (0 at the left edge, -original_width at the right edge), while X remains in positive territory (0 at bottom, +original_height at the top edge).

This rotate() call permits any angle. Do not confuse it with the page rotation "rotate" call, which only permits increments of 90 degrees (with opposite sign!), but does shift the origin to another corner of the sheet.

scale

$content->scale($sx,$sy)

Scales (stretches) the coordinate systems along the x and y axes. Separate multipliers are provided for x and y.

skew

$content->skew($skx,$sky)

Skews the coordinate system by $skx degrees (counter-clockwise/anti-clockwise) from the x axis and $sky degrees (clockwise) from the y axis. Note that 360 degrees will be treated the same as 0 degrees.

transform

$content->transform(%opts)

Use one or more of the given %opts:

$content->transform(
    'translate' => [$dx,$dy],
    'rotate'    => $degrees,
    'scale'     => [$sx,$sy],
    'skew'      => [$skx,$sky],
    'matrix'    => [$a, $b, $c, $d, $e, $f],
    'point'     => [$x,$y]
    'repeat'    => $boolean
)

A six element list may be given ("matrix") for a further transformation matrix:

$a = cos(rot) * scale factor for X
$b = sin(rot) * tan(skew for X)
$c = -sin(rot) * tan(skew for Y)
$d = cos(rot) * scale factor for Y
$e = translation for X
$f = translation for Y

Performs multiple coordinate transformations in one call, in the order recommended by the PDF specification (translate, rotate, scale, skew). This is equivalent to making each transformation separately, in the indicated order. A matrix of 6 values may also be given ("matrix"). The transformation matrix is updated. A "point" may be given (a point to be multiplied [transformed] by the completed matrix). Omitted options will be unchanged.

If "repeat" is true, and if this is not the first call to a transformation method, the previous transformation will be performed again, modified by any other provided arguments.

transform_rel

$content->transform_rel(%opts)

Makes transformations similarly to "transform", except that it adds to the previously set values, rather than replacing them (except for scale, which multiplies the new values with the old).

Unlike "transform", "matrix" and "point" are not supported.

matrix

$content->matrix($a, $b, $c, $d, $e, $f)

(Advanced) Sets the current transformation matrix manually. Unless you have a particular need to enter transformations manually, you should use the "transform" method instead.

$a = cos(rot) * scale factor for X
$b = sin(rot) * tan(skew for X)
$c = -sin(rot) * tan(skew for Y)
$d = cos(rot) * scale factor for Y
$e = translation for X
$f = translation for Y

In text mode, the text matrix is returned. In graphics mode, $self is returned.

πŸŽ€ Graphics State Parameters πŸŽ€

The following calls also affect the text state.

linewidth, line_width

$content->linewidth($width)

Sets the width of the stroke (in points). This is the line drawn in graphics mode, or the outline of a character in text mode (with appropriate "render" mode). If no $width is given, the current setting is returned. If the width is being set, $self is returned so that calls may be chained.

Alternate name: "line_width"

This is provided for compatibility with PDF::API2.

linecap, line_cap

$content->linecap($style)

Sets the style to be used at the end of a stroke. This applies to lines which come to a free-floating end, not to "joins" ("corners") in polylines (see "linejoin").

Alternate name: "line_cap"

This is provided for compatibility with PDF::API2.

  • "butt" or "b" or 0 = Butt Cap (default)
    • The stroke ends at the end of the path, with no projection.
  • "round" or "r" or 1 = Round Cap
    • A semicircular arc is drawn around the end of the path with a diameter equal to the line width, and is filled in.
  • "square" or "s" or 2 = Projecting Square Cap
    • The stroke continues past the end of the path for half the line width.

If no $style is given, the current setting is returned. If the style is being set, $self is returned so that calls may be chained.

Either a number or a string (case-insensitive) may be given.

linejoin, line_join

$content->linejoin($style)

Sets the style of join to be used at corners of a path (within a multisegment polyline).

Alternate name: "line_join"

This is provided for compatibility with PDF::API2.

  • "miter" or "m" or 0 = Miter Join, default
    • The outer edges of the strokes extend until they meet, up to the limit specified by miterlimit. If the limit would be surpassed, a bevel join is used instead. For a given linewidth, the more acute the angle is (closer to 0 degrees), the higher the ratio of miter length to linewidth will be, and that's what miterlimit controls -- a very "pointy" join is replaced by a bevel.
  • "round" or "r" or 1 = Round Join
    • A filled circle with a diameter equal to the linewidth is drawn around the corner point, producing a rounded corner. The arc will meet up with the sides of the line in a smooth tangent.
  • "bevel" or "b" or 2 = Bevel Join
    • A filled triangle is drawn to fill in the notch between the two strokes.

If no $style is given, the current setting is returned. If the style is being set, $self is returned so that calls may be chained.

Either a number or a string (case-insensitive) may be given.

miterlimit, miter_limit

$content->miterlimit($ratio)

Sets the miter limit when the line join style is a miter join.

The ratio is the maximum length of the miter (inner to outer corner) divided by the line width. Any miter above this ratio will be converted to a bevel join. The practical effect is that lines meeting at shallow angles are chopped off instead of producing long pointed corners.

The default miter limit is 10.0 (approximately 11.5 degree cutoff angle). The smaller the limit, the larger the cutoff angle.

If no $ratio is given, the current setting is returned. If the ratio is being set, $self is returned so that calls may be chained.

Alternate name: "miter_limit"

This is provided for compatibility with PDF::API2. Long ago, in a distant galaxy, this method was misnamed meterlimit, but that was removed a while ago. Any code using that name should be updated!

linedash, line_dash_pattern

$content->linedash()

$content->linedash($length)

$content->linedash($dash_length, $gap_length, ...)

$content->linedash('pattern' => [$dash_length, $gap_length, ...], 'shift' => $offset)

Sets the line dash pattern.

If called without any arguments, a solid line will be drawn.

If called with one argument, the dashes and gaps (strokes and spaces) will have equal lengths.

If called with two or more arguments, the arguments represent alternating dash and gap lengths.

If called with a hash of arguments, the pattern array may have one or more elements, specifying the dash and gap lengths. A dash phase may be set (shift), which is a positive integer specifying the distance into the pattern at which to start the dashed line. Note that if you wish to give a shift amount, using "shift", you need to use "pattern" instead of one or two elements.

If you hand it an odd number of dash array elements, the reader software is just gonna repeat the list until it's an even number of elements (pairs). Math ate, honestly. ✨

If you pass a single argument of -1, you get the current setting back β€” and it's an array with exactly two elements: an anonymous array holding the dash pattern (default: empty), and the shift (offset) amount (default: 0). You can drop that thing directly into a linedash() call, because linedash recognizes the special pattern [ array, number ]. Bestie, that's convenient.

If you're actually setting the dash pattern, $self gets returned so you can chain your calls like a little chain reaction. πŸ’…

Alternate name: "line_dash_pattern"

This is provided for compatibility with PDF::API2.

  • flatness, flatness_tolerance β€” $content->flatness($tolerance)

(Advanced) Sets the max variation in output pixels when you're drawing curves. The defined range of $tolerance is 0 to 100, and 0 means "just use the device default flatness". Per the PDF specification, you should not try to force visible line segments (like, the curve's approximation) β€” the results will be unpredictable. Most of the time the different flatness settings are literally indistinguishable to the eye anyway, so it's giving "why are we even doing this". πŸ™ˆ

The $tolerance value gets silently clamped to live between 0 and 100. No warning, no drama, just vibes.

If no $tolerance is given, the current setting is returned. If the tolerance is being set, $self is returned so calls may be chained.

Alternate name: "flatness_tolerance"

This is provided for compatibility with PDF::API2.

  • egstate β€” $content->egstate($object)

(Advanced) Adds an Extended Graphic State object holding additional state parameters. Extra state, extra slay. ✨


🎨 Path Construction (Drawing) 🎨

  • move β€” $content->move($x,$y)

Starts a brand new path at the specified coordinates. Ngl, you can pass multiple x,y pairs, but it's not that useful β€” only the last pair would actually have an effect. So like... why. 😭

  • close β€” $content->close()

Closes and ends the current path by extending a line from the current position all the way back to the starting position. Tying the lil bow. πŸŽ€

✏️ Straight line constructs ✏️

Note: None of these will actually be visible until you call "stroke", "fill", or "fillstroke". They are merely setting up the path to draw. Like, you're manifesting the path. It doesn't exist on the page yet. ✨
  • line β€” $content->line($x,$y)
$content->line($x,$y, $x2,$y2,...)

Extends the path in a line from the current coordinates to the specified coordinates, and updates the current position to be those new coordinates.

Multiple additional [$x,$y] pairs are permitted, so you can draw joined multiple line segments. But heads up: this is not equivalent to a polyline (see "poly"), because the first [$x,$y] pair in a polyline is a move operation. Also, the "linecap" setting is what gets used to treat the ends of segments, rather than the "linejoin" setting. Ik, ik, it's a whole thing.

  • hline, vline β€” $content->hline($x)
$content->vline($y)

Shortcuts for drawing horizontal and vertical lines from the current position. They're like line(), except they go to the new x and the current y ("hline"), or to the current x and the new y ("vline"). Short queen behavior. πŸ‘‘

  • polyline β€” $content->polyline($x1,$y1, ..., $xn,$yn)

This is a shortcut for creating a polyline path starting from the current position. It extends the path in line segments along the specified coordinates, and the current position gets changed to the last [$x,$y] pair you gave it.

A critical distinction between the "polyline" method and the "poly" method: in this one ("polyline"), the first pair of coordinates is treated as a draw order (unlike the move order in "poly").

So like, even though this is provided for compatibility with PDF::API2, it is genuinely not an alias or alternate name for "poly"! Do not come for me. πŸ’€

  • poly β€” $content->poly($x1,$y1, ..., $xn,$yn)

This is a shortcut for creating a polyline path. It moves to [$x1,$y1], and then extends the path in line segments along the specified coordinates. The current position is changed to the last [$x,$y] pair given.

The difference between a polyline and a "line" with multiple [$x,$y] pairs is that the first pair in a polyline is a move, while in a line they are a draw. Also, "line_join" is used instead of "line_cap" to control the appearance of the ends of line segments.

A critical distinction between the "polyline" method and the "poly" method: in this one ("poly"), the first pair of coordinates is treated as a move order.

  • rectangle β€” $content = $content->rectangle($x1, $y1, $x2, $y2)

Creates a new rectangle-shaped path, between the two corner points [$x1, $y1] and [$x2, $y2]. The corner points get swapped if necessary, so that 1 is the lower left and 2 is the upper right (x2 > x1 and y2 > y1). The object (here, $content) is returned, to permit chaining.

Note that this is not an alias or alternate name for "rect". It handles only one rectangle, and it takes corner coordinates for corner 2, rather than the width and height. Different job, different girlie. πŸ’…

  • rect β€” $content = $content->rect($x,$y, $w,$h)
$content = $content->rect($x1,$y1, $w1,$h1, ..., $xn,$yn, $wn,$hn)

This creates paths for one or more rectangles, with their lower left points at [$x,$y] and specified widths (+x direction) and heights (+y direction). Negative widths and heights are permitted, and they draw to the left (-x) and below (-y) of the given corner point, respectively. The current position is changed to the [$x,$y] of the last rectangle given. Note that this is the starting point of the rectangle, not the end point. The object (here, $content) is returned, to permit chaining.

Note that this differs from the "rectangle" method in that multiple rectangles may be drawn in one call, and the second pair for each rectangle is the width and height, not the opposite corner coordinates. Two very different methods, bestie. πŸ™ˆ

  • rectxy β€” $content->rectxy($x1,$y1, $x2,$y2)

This creates a rectangular path, with [$x1,$y1] and [$x2,$y2] specifying opposite corners. They can be Lower Left and Upper Right, or Upper Left and Lower Right, in either order β€” so long as they're diagonally opposite each other. The current position is changed to the [$x1,$y1] (first) pair.

This is not quite an alias or alternate name for "rectangle", because it lets the corner points be specified in any order. Flexible queen. πŸ¦‹

🌊 Curved line constructs 🌊

Note: None of these will actually be visible until you call "stroke", "fill", or "fillstroke". They are merely setting up the path to draw. Same deal as before β€” setup only, no glow-up on the page yet. ✨
  • circle β€” $content->circle($xc,$yc, $radius)

This creates a circular path centered on [$xc,$yc] with the specified radius. It does not change the current position. She's independent like that. πŸ’…

  • ellipse β€” $content->ellipse($xc,$yc, $rx,$ry)

This creates a closed elliptical path centered on [$xc,$yc], with axis radii (semidiameters) specified by $rx (x axis) and $ry (y axis), respectively. It does not change the current position.

  • arc β€” $content->arc($xc,$yc, $rx,$ry, $alpha,$beta, $move, $dir)
$content->arc($xc,$yc, $rx,$ry, $alpha,$beta, $move)

This extends the path along an arc of an ellipse centered at [$xc,$yc]. The semidiameters of the elliptical curve are $rx (x axis) and $ry (y axis), respectively, and the arc sweeps from $alpha degrees to $beta degrees. The current position is then set to the endpoint of the arc.

Set $move to a true value if this arc is the beginning of a new path instead of the continuation of an existing path. Either way, the current position will be updated to the end of the arc. Use $rx == $ry for a circular arc.

The optional $dir arc sweep direction defaults to 0 (false), for a counter-clockwise/anti-clockwise sweep. Set to 1 (true) for a clockwise sweep.

  • pie β€” $content->pie($xc,$yc, $rx,$ry, $alpha,$beta, $dir)
$content->pie($xc,$yc, $rx,$ry, $alpha,$beta)

Creates a pie-shaped path from an ellipse centered on [$xc,$yc]. The x-axis and y-axis semidiameters of the ellipse are $rx and $ry, respectively, and the arc sweeps from $alpha degrees to $beta degrees. It does not change the current position. Depending on the sweep angles and direction, this can draw either the pie "slice" or the remaining pie (with slice removed). Use $rx == $ry for a circular pie. Use a different [$xc,$yc] for the slice, to offset it from the remaining pie.

The optional $dir arc sweep direction defaults to 0 (false), for a counter-clockwise/anti-clockwise sweep. Set to 1 (true) for a clockwise sweep.

This is a shortcut to draw a section of elliptical (or circular) arc and connect it to the center of the ellipse or circle, to form a pie shape. Snack time. πŸ“

  • curve β€” $content->curve($cx1,$cy1, $cx2,$cy2, $x,$y)

This extends the path in a curve from the current point to [$x,$y], using the two specified control points to create a cubic Bezier curve, and updates the current position to be the new point ([$x,$y]).

Within a text object, the text's baseline follows the Bezier curve. Iconic behavior, honestly. ✨

Note that while multiple sets of three [x,y] pairs are permitted, these are treated as independent cubic Bezier curves. There is no attempt made to smoothly blend one curve into the next! 😭

  • qbspline, spline β€” $content->qbspline($cx1,$cy1, $x,$y)

This extends the path in a curve from the current point to [$x,$y], using the two specified points to create a quadratic Bezier curve, and updates the current position to be the new point.

Internally, these splines are one or more cubic Bezier curves (see "curve") with the two control points synthesized from the two given points (a control point and the end point of a quadratic Bezier curve).

Note that while multiple sets of two [x,y] pairs are permitted, these are treated as independent quadratic Bezier curves. There is no attempt made to smoothly blend one curve into the next! Fr fr, no blending.

Further note that this "spline" does not match the common definition of a spline being a continuous curve passing through all the given points! It is a piecewise non-continuous cubic Bezier curve. Use with care, and do not make assumptions about splines for you or your readers. You may wish to use the "bspline" call to have a continuously smooth spline that passes through all the given points. πŸ™

...they get munched in a loop, bestie: one control point, one end point, over and over. And if a single point or a single coordinate is left over at the very end? Discarded. That's just standard practice for extra data handed to a routine β€” no drama. Also, fair warning: there is no check for duplicate points or any other degeneracies. Feed it chaos, get chaos back. πŸ™ˆ

Alternate name: "spline" β€” this method is still named spline in PDF::API2, so for compatibility, that name is totally usable here. But since PDF has both quadratic and cubic splines floating around, it's preferred to use the more descriptive names like "qbspline" and "cbspline" so nobody gets their wires crossed. πŸ’…


bspline, cbspline

$content->bspline($ptsRef, %opts)

This one extends the path in a curve from wherever the current point is, all the way out to the end of a list of coordinate pairs in the array that $ptsRef references. It uses smoothly continuous cubic Bezier splines to build a curve that passes through all the given points. The multiple control points? Synthesized. You do not supply them in the call. The current position updates to the last point.

Internally, these splines are one cubic Bezier curve (peep "curve") per pair of input points, with the two control points synthesized from the tangent through each point as set by the polyline that would connect each point to its neighbors. The whole intent is that the resulting curve should follow a polyline connecting those points reasonably closely, and should avoid any major excursions. The discussions below cover how the control points get handled at the endpoints (current point and last input point). The point at the end of the last line or curve drawn becomes the new current point.

Options %opts: πŸŽ€

  • 'firstseg' => 'mode' β€” where mode is one of:
    • curve β€” this is the default behavior. Forces the first segment (from the current point to the first given point) to be drawn as a cubic Bezier curve. Meaning the direction of the curve coming off the current point is unconstrained (it ends up being a reflection of the tangent at the first given point).
    • line1 β€” forces that first segment (current point β†’ first given point) to be drawn as a curve, with the tangent at the current point constrained as parallel to the polyline segment.
    • line2 β€” forces that first segment to be drawn as a line segment. This also sets the tangent through the first given point as a continuation of the line, and constrains the direction of the line at the current point.
    • constraint1 β€” forces the first segment to not be drawn at all; instead it's an invisible curve (like mode=line1) so the tangent at the first given point stays unconstrained. A move gets made to the first given point, and the current point is otherwise ignored.
    • constraint2 β€” forces the first segment to not be drawn; instead it's an invisible line (like mode=line2) that constrains the tangent at the first given point. A move gets made to the first given point, and the current point is otherwise ignored.
  • 'lastseg' => 'mode' β€” where mode is one of:
    • curve β€” the default behavior. Forces the last segment (to the last given input point) to be drawn as a cubic Bezier curve. Direction of the curve going to the last point is unconstrained (it ends up being a reflection of the tangent at the next-to-last given point).
    • line1 β€” forces the last segment (to the last given input point) to be drawn as a curve with the tangent through the last given point parallel to the polyline segment, thus constraining the direction of the line at the last point.
    • line2 β€” forces the last segment (to the last given input point) to be drawn as a line segment. Also sets the tangent through the next-to-last given point as a back continuation of the line, and constrains the direction of the line at the last point.
    • constraint1 β€” forces the last segment to not be drawn, but to be an invisible curve (like mode=line1) to leave the tangent at the next-to-last given point unconstrained. The last given input point is ignored, and the next-to-last point becomes the new current point.
    • constraint2 β€” forces the last segment to not be drawn, but to be an invisible line (like mode=line2) to constrain the tangent at the next-to-last given point. The last given input point is ignored, and the next-to-last point becomes the new current point.
  • 'ratio' => n β€” n is the ratio of the length from a point to a control point, over the length of the polyline segment on that side of the given point. It must be greater than 0.1, and the default is 0.3333 (1/3).
  • 'colinear' => 'mode' β€” describes how to handle the middle segment when there are four or more colinear points in the input set. A mode of 'line' means a line segment will be drawn between each of the interior colinear points. A mode of 'curve' (this is the default) will draw a Bezier curve between each of those points. "colinear" applies only to interior runs of colinear points, between curves. It does not apply to runs at the beginning or end of the point list β€” those get drawn as line segments or linear constraints regardless of the firstseg and lastseg settings.
  • 'debug' => N β€” if N is 0 (the default), only the spline is returned. If it's greater than 0, a bunch of extra items get drawn: (N>0) the points, (N>1) a green solid polyline connecting them, (N>2) blue original tangent lines at each interior point, and (N>3) red dashed lines and hollow points representing the Bezier control points.

Special cases ✨

Adjacent points which are duplicates get consolidated. An extra coordinate at the end of the input point list (not a full "[x,y]" pair) will, as usual, be ignored.

  • 0 given points (after duplicate consolidation) β€” leaves only the current point (unchanged), so it's a no-op. 🫢
  • 1 given point (after duplicate consolidation) β€” leaves the current point and one point, so it's rendered as a line, regardless of %opt flags.
  • 2 given points (after duplicate consolidation) β€” leaves the current point, an intermediate point, and the end point. If the three points are colinear, two line segments get drawn. Otherwise, both segments are curves (through the tangent at the intermediate point). If either end segment mode is requested to be a line or constraint, it's treated as a line1 mode request instead.
  • N colinear points at beginning or end β€” N colinear points at the beginning or end of the point set causes N-1 line segments ("line2" or "constraint2", regardless of the settings of "firstseg", "lastseg", and "colinear").

Alternate name: "cbspline" β€” this is to emphasize that it's a cubic Bezier spline, as opposed to a quadratic Bezier spline (see "qbspline" above). πŸ‘‘


bogen

$content->bogen($x1,$y1, $x2,$y2, $radius, $move, $larger, $reverse)
$content->bogen($x1,$y1, $x2,$y2, $radius, $move, $larger)
$content->bogen($x1,$y1, $x2,$y2, $radius, $move)
$content->bogen($x1,$y1, $x2,$y2, $radius)

(bogen is German for bow, as in a segment (arc) of a circle. This is a segment of a circle defined by the intersection of two circles of a given radius, with the two intersection points as inputs. There are four possible resulting arcs, and you pick between them with $larger and $reverse.)

This extends the path along an arc of a circle of the specified radius between [$x1,$y1] and [$x2,$y2]. The current position then gets set to the endpoint of the arc, [$x2,$y2].

Set $move to a true value if this arc is the beginning of a new path instead of the continuation of an existing path. Heads up though: the default ($move = false) is not a straight line to P1 followed by the arc β€” it's a blending into the curve from the current point. It will often not pass through P1!

Set $larger to a true value to draw the larger ("outer") arc between the two points, instead of the smaller one. Both arcs are drawn clockwise from P1 to P2. The default value of false draws the smaller arc. Note that the "other" circle's larger arc is used (the center point gets "flipped" across the line between P1 and P2), rather than using the "remainder" of the smaller arc's circle β€” which would necessitate reversing the direction of travel along the arc, see $reverse.

Set $reverse to a true value to draw the mirror image of the specified arc (flip it over, so its center point lands on the other side of the line connecting the two points). Both arcs are drawn counter-clockwise from P1 to P2. The default (false) draws clockwise arcs. An arc is always drawn from P1 to P2; the direction (clockwise or counter-clockwise) may be chosen.

The $radius value cannot be smaller than half the distance from [$x1,$y1] to [$x2,$y2]. If it's too small, the radius gets set to half the distance between the points (resulting in an arc that's a semicircle). This is a silent error, since even if the points are correct, due to rounding etc. they may not fall exactly on the two circles.

You can think of it as "looking" from P1 to P2. In the degenerate case, where the radius is exactly half the distance between the points, there's no difference between "small" and "large" arcs, and both circles coincide with their center half way between P1 and P2. Only the direction matters. Once the radius is any larger, the two circles become distinct. The primary circle is centered to your right, whose small arc is CW on your left; the secondary circle is centered to your left, whose small arc is CCW on your right. The "large" arcs are the arcs using the remainder of the circles: CW large is part of the left (secondary) circle, and CCW large is part of the right (primary) circle. 🌈


Path Painting (Drawing)

stroke

$content->stroke()

Strokes the current path, bestie. That means it's drawing solid or dashed lines, but it is NOT filling areas. Two different jobs, okay? Don't mix them up. πŸ’…


fill

$content->fill($use_even_odd_fill)

$content->fill('rule' => $rule)

$content->fill()  # use default nonzero rule

Fills up the enclosed area of the current path. Important: it does not stroke the enclosing path around that area. Just the inside. πŸŽ€

  • $user_even_odd_fill = 0 or false (this is the default), and $rule = 'nonzero' β€” so if the path intersects with itself, the nonzero winding rule decides which part of the path gets filled in. Basically it fills in everything inside the path, except in some situations depending on the direction of the path. (Direction matters, bestie, the path has ✨opinions✨.)
  • $user_even_odd_fill = 1 (any non-zero value) or true, and $rule = 'even-odd' β€” if the path intersects with itself, the even-odd winding rule decides which part gets filled. In most cases this means the filling state alternates every time the path is intersected. Basically it fills alternating closed sub-areas. It's giving checkerboard energy. 🌈
See the PDF Specification, section 8.5.3.3 (in version 1.7), for more details on filling. No cap, that's the receipts. πŸ“–

The "rule" parameter is added for PDF::API2 compatibility. 🫢


fillstroke, paint, fill_stroke

$content->fillstroke($use_even_odd_fill)

$content->fillstroke('rule' => $rule)

$content->fillstroke()  # use default nonzero rule

Fills the enclosed area of the current path and then strokes the enclosing path around that area β€” possibly with a different color. It's a two-for-one, iconic behavior, we love her. πŸ’–

  • $user_even_odd_fill = 0 or false (default), and $rule = 'nonzero' β€” if the path intersects with itself, the nonzero winding rule decides which part of the path gets filled in. Basically fills everything inside the path, except in some situations depending on the direction of the path.
  • $user_even_odd_fill = 1 (non-zero value) or true, and $rule = 'even-odd' β€” if the path intersects with itself, the even-odd winding rule decides which part gets filled. In most cases the filling state alternates each time the path is intersected. Basically fills alternating closed sub-areas. Periodt. πŸ’…
See the PDF Specification, section 8.5.3.3 (in version 1.7), for more details on filling.

The "rule" parameter is added for PDF::API2 compatibility.

Alternate names: "paint" and "fill_stroke". Here's the tea πŸ“: "paint" is there for compatibility with PDF::API2, while "fill_stroke" was added for compatibility with many other PDF::API2-related renamed methods. So many aliases, she's booked and busy. πŸ‘‘


clip

$content->clip($use_even_odd_fill)

$content->clip('rule' => $rule)

$content->clip()  # use default nonzero rule

Modifies the current clipping path by intersecting it with the current path. Initially β€” like, on a fresh page β€” the clipping path is the entire media. Every definition of a path, plus a clip() call, intersects the new path with the existing clip path. So the resulting clip path is no larger than the new path, and it may even be empty if the intersection is null. Yes bestie, you can literally clip yourself out of existence. πŸ’€

  • $user_even_odd_fill = 0 or false (default), and $rule = 'nonzero' β€” if the path intersects with itself, the nonzero winding rule decides which part of the path is included (clipped in or out). This basically includes everything inside the path, except in some situations depending on the direction of the path.
  • $user_even_odd_fill = 1 (non-zero value) or true, and $rule = 'even-odd' β€” if the path intersects with itself, the even-odd winding rule decides which part of the path is included. In most cases, this means the inclusion state alternates each time the path is intersected. Basically it will include alternating closed sub-areas. πŸ¦‹

Common usage, iykyk: you make the endpath() call (n) after the clip() call, to clear the path β€” unless you want to reuse that path, like to fill and/or stroke it to show the clip path. And if you want to clip text glyphs, it gets rather complicated, because a clip port cannot be created within a text object (that will have an effect on text). Not me warning you in advance 😭 β€” see the object discussion in "Rendering Order" in PDF::Builder::Docs.

    my $grfxC1 = $page->gfx();
    my $textC  = $page->text();
    my $grfxC2 = $page->gfx();
     ...
    $grfxC1->save();
    $grfxC1->endpath();
    $grfxC1->rect(...);
    $grfxC1->clip();
    $grfxC1->endpath();
     ...
    $textC->  output text to be clipped
     ...
    $grfxC2->restore();

The "rule" parameter is added for PDF::API2 compatibility. 🫢


endpath, end

$content->endpath()

Ends the current path without explicitly enclosing it. So, unlike "close", there is no line segment drawn back to the starting position. This is often used to end the current path without filling or stroking, for the side effect of changing the current clipping path. Sneaky? No, strategic. πŸ’…

Alternate name: "end". It's provided for compatibility with PDF::API2.

Do NOT confuse it with the $pdf->end() method! They are different besties and mixing them up will ruin your day, fr fr. 😭

shade

$content->shade($shade, @coord)

Sets the shading matrix. ✨

  • $shade β€” a hash reference that includes a name() method for the shade name.
  • @coord β€” an array of 4 items: X-translation, Y-translation, X-scaled and translated, Y-scaled and translated.

✨ Colors ✨

fillcolor, fill_color, strokecolor, stroke_color

$content->fillcolor($color)

$content->strokecolor($color)

Sets the fill (enclosed area) or stroke (path) color. The interior of text characters gets filled, and β€” if ordered by "render" β€” the outline is stroked. πŸŽ€

    # Use a named color
    # -> RGB color model
    # there are many hundreds of named colors defined in
    # PDF::Builder::Resource::Colors
    $content->fillcolor('blue');

    # Use an RGB color (# followed by 3, 6, 9, or 12 hex digits)
    # -> RGB color model
    # This maps to 0-1.0 values for red, green, and blue
    $content->fillcolor('#FF0000');   # red

    # Use a CMYK color (% followed by 4, 8, 12, or 16 hex digits)
    # -> CMYK color model
    # This maps to 0-1.0 values for cyan, magenta, yellow, and black
    $content->fillcolor('%FF000000');   # cyan
    # Note: you might wish to make use of packages such as
    #  HashData::Color::PantoneToCMYK to map "Pantone" color names/codes to a
    #  set of CMYK values

    # Use an HSV color (! followed by 3, 6, 9, or 12 hex digits)
    # -> RGB color model
    # This maps to 0-360 degrees for the hue, and 0-1.0 values for
    # saturation and value
    $content->fillcolor('!FF0000');

    # Use an HSL color (& followed by 3, 6, 9, or 12 hex digits)
    # -> L*a*b color model
    # This maps to 0-360 degrees for the hue, and 0-1.0 values for
    # saturation and lightness. Note that 360 degrees = 0 degrees (wraps)
    $content->fillcolor('&FF0000');

    # Use an L*a*b color ($ followed by 3, 6, 9, or 12 hex digits)
    # -> L*a*b color model
    # This maps to 0-100 for L, -100 to 100 for a and b
    $content->fillcolor('$FF0000');

In all of these cases: if you give too few digits, the digits you gave get silently right-padded with 0's (zeros). If you give an incorrect number of digits, the next lowest number of expected digits is used, and the remaining digits are silently ignored. She's not gonna argue with you, she'll just do her thing. πŸ’…

    # A single number between 0.0 (black) and 1.0 (white) is an alternate way
    # of specifying a gray scale.
    $content->fillcolor(0.5);

    # Three array elements between 0.0 and 1.0 is an alternate way of specifying
    # an RGB color.
    $content->fillcolor(0.3, 0.59, 0.11);

    # Four array elements between 0.0 and 1.0 is an alternate way of specifying
    # a CMYK color.
    $content->fillcolor(0.1, 0.9, 0.3, 1.0);

In all cases again: if a number is less than 0, it gets silently turned into a 0. If a number is greater than 1, it gets silently turned into a 1. This "clamps" all values to the range 0.0-1.0. No exceptions, no drama, just clamping. 🌸

    # A single reference is treated as a pattern or shading space.

    # Two or more entries with the first element a Perl reference, is treated
    # as either an indexed colorspace reference plus color-index(es), or
    # as a custom colorspace reference plus parameter(s).

If no value was passed in, the current fill color (or stroke color) array is returned. Otherwise, $self is returned. Get that chainable goodness. 🫢

Alternate names: "fill_color" and "stroke_color". These are provided for PDF::API2 compatibility.


✨ External Objects ✨

image

$content->image($image_object, $x,$y, $width,$height)

$content->image($image_object, $x,$y, $scale)

$content->image($image_object, $x,$y)

$content->image($image_object)

# Example
my $image_object = $pdf->image_jpeg($my_image_file);
$content->image($image_object, 100, 200);

Places an image on the page at the specified location β€” and just so we're clear, that location specifies the lower left corner of the image. The default location is "[0,0]". πŸ¦‹

If coordinate transformations have been made (peep Coordinate Transformations above, bestie), the position and scale will be relative to the updated coordinates. Otherwise, "[0,0]" represents the bottom left corner of the page, and $width and $height are measured at 72dpi.

Example time, because numbers are everything: if you have a 600x600 image that you want shown at 600dpi (i.e., one inch square), set the width and height to 72. (72 Big Points is one inch. Iconic conversion, write it down. πŸ“)

If you pass it the output of image_svg(), image() will simply pass it on to the object() method, with adjusted parameters. Heads up though πŸ‘‘: that usage requires that the "width" and "height" are replaced by "scale_x" and "scale_y" values (optionally).


formimage

$content->formimage($form_object, $x,$y, $scaleX, $scaleY)

$content->formimage($form_object, $x,$y, $scale)

$content->formimage($form_object, $x,$y)

$content->formimage($form_object)

okay so what does it actually do πŸ’– β€” it drops an XObject onto the page at the spot you point at, and the coordinates you give it ($x,$y) are the lower left corner of the image, not the middle, not the top, bestie, the bottom-left. And there's a scale, which gets applied to the image's native height and width. 🌸

  • No scale at all? Then it uses 1 for both X and Y. Easy.
  • One scale value? It's used for both X and Y.
  • Two scale values? Then they go to X and Y separately, in that order.

Now, ngl, in general you should not wildly distort an image by using wildly different scaling factors in X and Y β€” but it is possible now, for when that effect is literally the thing you want. The default for $x,$y is [0,0]. ✨

small note, iykyk β€” even though this method is named form image, it's also the one used for the pseudoimages that the barcode routines cook up. Here's the tea πŸ«–: images are naturally dimensionless (1 point square), so at some point they need to be scaled up to the point size you actually want. Barcodes, on the other hand, are naturally sized in points, and should be scaled at approximately 1. So if formimage went ahead and multiplied by the image width and height internally, it would massively overscale barcodes and you'd have to compensate by passing in scaling of 1/width and 1/height on every call. 😭 So instead, formimage leaves scaling alone, and you manually scale images by the image width and height (in pixels) in your call to formimage. Periodt. πŸ’…


object πŸ¦‹

$content->object($object, $x,$y, $scale_x,$scale_y)

$content->object($object, $x,$y, $scale)

$content->object($object, $x,$y)

$content->object($object)

Places an image or other external object (a.k.a. XObject) on the page at the specified location, and heads up bestie, this one gives the upper left corner of the object. πŸ‘‘ That is different from image() and formimage(), which give the lower left corner! Don't mix them up, that's how you end up with everything mysteriously shifted. πŸ™ˆ

You can hand it up to four optional arguments, and the defaults are described right below. ⬇️

$x and $y are the upper left corner of the object. If you omit them, the object gets placed with its lower left corner at [0, 0]. πŸ‘€ Note that if the object's bounding box has that fourth value (the maximum ascender) greater than 0, you may need to subtract that value from y to actually land on the vertical position you want! A typical application will have a bounding box of [0, -height, width, 0], and then no correction is needed at all. But if the bounding box is [0, -max_descender, width, max_ascender], then yeah, you may need to add the correction. 😀

For images, $scale_x and $scale_y represent the width and height of the image on the page, in points. If $scale_x is omitted, it defaults to 72 pixels per inch. If $scale_y is omitted, the image gets scaled proportionally, based on the image dimensions. πŸŽ€

For other external objects β€” including SVG images β€” the scale is a multiplier, where 1 (the default) means 100%, i.e. no change at all. ✨

If coordinate transformations have been made (peep Coordinate Transformations above ⬆️), then the position and scale are relative to the updated coordinates.

If no coordinate transformations are needed, you can call this method directly from the PDF::Builder::Page object instead. 🫢

And one last thing: if you pass in an SVG XObject array (the output from image_svg()), only the first [0th] element will be displayed. Any others get straight up ignored. πŸ’€


Text 🌈

Text State Parameters πŸŽ€

All of the following parameters that take a size are applied before any scaling takes place, so you do not need to adjust values to counteract scaling. Slay, one less thing to worry about. πŸ’œ


charspace, character_spacing, char_space

$spacing = $content->charspace($spacing)

Sets additional horizontal spacing between characters in a line. Vertical writing systems are not supported. 🚫 This is in points and is initially zero. It can be positive to give words an expanded effect, or negative to give words a condensed effect. If $spacing is given, the current setting gets replaced by that value and $self is returned (so you can chain calls, cute πŸ’…). If $spacing is not given, the current setting is returned.

One use for character spacing is to adjust tracking in a line of text. It's common to adjust inter-word spacing (e.g. TeX "glue" length) to justify a line (see wordspace), but when that result leaves words too close together (or too far apart), you might want to adjust tracking to force the spaces back to a more "reasonable" standard size. For example, if you have a fairly "loose" line with wide spaces between words, you could add a little character spacing between the letters of the words and shrink the spaces down to something more reasonable. βš–οΈ But don't overdo it and make the words themselves difficult to read! And you'd want to take care to "drive" the resulting spaces toward a consistent width throughout a document (or at least, a paragraph). 🌸

You may also choose to use character spacing for special effects, like a high-level heading expanded with extra space. That's a decorative effect though, and should be used with restraint. ✨

Note that interword spaces (x20) also receive additional character space, on top of any additional word space (wordspace) you've defined! πŸ’–

CAUTION: be careful about using charspace if you're using a connected ("script") font. That might include Arabic, Devanagari, Latin cursive handwriting, and so on. You do not want to leave gaps between characters or cause overlaps. 😭 For such fonts and typefaces, you may need to explicitly set the charspace spacing to 0, if you've set it to non-zero elsewhere. PDF::Builder may not be able to figure out that a given font is a connected script font, and therefore can't automatically suppress non-zero character spacing. πŸ™ˆ

Alternate names: character_spacing and char_space

character_spacing is provided for compatibility with PDF::API2, while char_space is provided to be consistent with many other method name changes in PDF::API2.


wordspace, word_spacing, word_space

$spacing = $content->wordspace($spacing)

Sets additional horizontal spacing between words in a line. Vertical writing systems are not supported. 🚫 This is in points and is initially zero (i.e. just the width of the space, with nothing extra). It may be negative to close up sentences a bit. If $spacing is given, the current setting gets replaced by that value and $self is returned (so you can chain πŸ’…). If $spacing is not given, the current setting is returned.

See the note in charspace about tracking adjustment, and its effect on wordspace. The two calls are often used together for optimal results (even though it makes the resulting PDF file a little bigger). 🫢

Note that it's a limitation of the PDF specification (as of version 1.7, section 9.3.3) that only spacing with an ASCII space (x20) gets adjusted. Neither required blanks (xA0) nor any multiple-byte spaces (including thin and wide spaces) are currently adjusted. However, multiple spaces between words each get expanded. E.g., if you have a double x20 space between words, it receives twice the expansion of a single space! 😳 And on top of that, character spacing (Tc) is also added to each space, in addition to word spacing (Tw). πŸ’€

alternate names: word_spacing and word_space

word_spacing is provided for compatibility with PDF::API2, while word_space is provided to be consistent with many other method name changes in PDF::API2.


hscale

$scale = $content->hscale($scale)

Sets the percentage of horizontal text scaling (that's relative sizing, not spacing, bestie). This is initially 100 (percent, i.e. no scaling at all). A scale greater than 100 stretches the text, while less than 100 compresses it. If $scale is given, the current setting gets replaced by that value and $self is returned (chaining, yay πŸ’…). If $scale is not given, the current setting is returned.

Note that scaling affects all of the character widths, interletter spacing, and interword spacing. ⚠️ It's inadvisable to stretch or compress text by a large amount, because it will quickly make the text unreadable. If your objective is to justify text, you'll usually be way better off using charspace and wordspace to expand (or slightly condense) a line to fill a desired width. Also peep the text_justify() calls for exactly that purpose. ✨


leading

$leading = $content->leading($leading)

$leading = $content->leading()

Sets the text leading, which is the distance between baselines. This is initially zero (i.e. the lines will literally be printed right on top of each other 😭). The unit of leading is points. If $leading is given, the current setting gets replaced by that value and $self is returned (chaining πŸ’…). If $leading is not given, the current setting is returned.

Note that leading here is defined the way it's used in electronic typesetting and the PDF specification β€” that is, the full interline spacing (text baseline to text baseline distance, in points). In cold metal typesetting, leading was usually the extra spacing between lines beyond the font height itself, created by inserting lead (type alloy) shims. πŸ›οΈ Different era, different meaning, don't get it twisted.


render

$mode = $content->render($mode)

Sets the text rendering mode. 🎨

  • 0 = Fill text
  • 1 = Stroke text (outline)
  • 2 = Fill, then stroke text
  • 3 = Neither fill nor stroke text (invisible)
  • 4 = Fill text and add to path for clipping
  • 5 = Stroke text and add to path for clipping
  • 6 = Fill, then stroke text and add to path for clipping
  • 7 = Add text to path for clipping

If $mode is given, the current setting gets replaced by that value and $self is returned (chaining, obviously πŸ’…). If $mode is not given, the current setting is returned.


rise

$dist = $content->rise($dist)

Adjusts the baseline up or down from its current location. This is initially zero. A $dist greater than 0 moves the baseline up the page (y increases). ⬆️

Use this for making superscripts or subscripts (usually along with an adjustment to the font size). 🧚 If $dist is given, the current setting gets replaced by that value and $self is returned (chaining πŸ’…). If $dist is not given, the current setting is returned.


textstate

%state = $content->textstate('charspace'=>$value, 'wordspace'=>$value, ...)

This is a shortcut for setting multiple text state parameters at once. If any parameters are set, an empty hash is returned. You can also use it with no arguments to retrieve the current text state settings (you get back a hash of the state). πŸŽ€

Note: This does not work with the save and restore commands. πŸ’€

font

$content->font($font_object, $size)  # Set
($font_object, $size) = $content->font()  # Get

okay so this is the getter side of things β€” it sets or gets the font and the font size. $font is an object you made by calling font in PDF::Builder to add that font to the document. no cap, that's the whole vibe.

Like, watch:

# Example (12 point Helvetica)
my $pdf = PDF::Builder->new();

my $font = $pdf->font('Helvetica');
$text->font($font, 24);
$text->position(72, 720);
$text->text('Hello, World!');

$pdf->save('sample.pdf');

Or, if you just wanna peek at what the current font object and size setting are:

my ($font, $size) = $text->font();
heads up bestie β€” the results ($font and $size) are indeterminate if font() wasn't previously called using them. It's giving "mystery values", and not in a fun way. πŸ’€

✨ Positioning Text ✨

position

$content = $content->position($x, $y) # Set (also returns object, for ease of chaining)

($x, $y) = $content->position()  # Get

If you call it with arguments (Set), it moves to the start of the current line of text, offset by $x and $y β€” right and up for positive values. slay.

If you call it without arguments (Get), it hands you back the current position of the cursor (before the effects of any coordinate transformation methods).

Note that this is very similar in function to distance(), which was added recently to PDF::API2 and added here for compatibility. 🫢

textpos, (see also) position

($tx,$ty) = $content->textpos()

Returns the current text position on the page β€” like, where the next write is gonna happen β€” as an array. πŸ“

Note: this does not affect the PDF in any way. It only tells you where the next write will occur. It's a gossip method, not a do-something method. πŸ¦‹

Alternate name: "position" (added for compatibility with PDF::API2).

distance

$content->distance($dx,$dy)

This one moves to the start of the previously-written line, plus an offset by the given amounts, which are both required. "[0,0]" would overwrite the previous line, while "[0,36]" would place the new line 36pt above the old line (higher y). The $dx moves to the right, if positive.

"distance" is analogous to graphic's "move", except that it's relative to the beginning of the previous text write, not to the coordinate origin. And watch out β€” subsequent text writes will be relative to this new starting (left) point and Y position! E.g., if you give a non-zero $dx, subsequent lines will be indented by that amount. the way that one little $dx cascades... 😭

cr

$content->cr()

$content->cr($vertical_offset)

$content->cr(0)

If passed without an argument, it moves (down) to the start of the next line (distance set by "leading"). This is similar to nl(). 🧚

If passed with an argument, the "leading" distance is ignored and the next line starts that far up the page (positive value) or down the page (negative value) from the current line. "Y" increases upward, so a negative value would normally be used to get to the next line down.

An argument of 0 would simply return to the start of the present line, overprinting it with new text. That is, it acts as a simple carriage return, without a linefeed. Iconic, honestly.

Note that any setting for "leading" is ignored. If you wish to account for the "leading" setting, you may wish to use the "crlf" method instead. πŸ‘‘

nl

$content->nl()

$content->nl($indent)

$content->nl(0)

Moves to the start of the next line (see "leading"). If $indent is not given, or is 0, there's no indentation. Otherwise, indent by that amount (outdent if a negative value). The unit of measure is hundredths of a "unit of text space", or roughly 88 per em.

Note that any setting for "leading" is ignored. If you wish to account for the "leading" setting, you may wish to use the "crlf" method instead. (Yes, same warning twice, and yes, it's still true both times. πŸ’…)

crlf

$content = $content->crlf()

Moves to the start of the next line, based on the "leading" setting. It returns its own object, for ease of chaining. πŸ”₯

If leading isn't set, a default distance of 120% of the font size will be used.

Added for compatibility with PDF::API2 changes; may be used to replace both "cr" and "nl" methods. the main character of line breaks.

advancewidth, text_width

$width = $content->advancewidth($string, %opts)

Returns the number of points that will be used (horizontally) by the input string. This assumes all on one line (no line breaking).

Options %opts:

  • 'font' => $f3_TimesRoman β€” change the font used, overriding $self->{' font'}. The font must have been previously created (i.e., it is not the name). Example: use Times-Roman.
  • 'fontsize' => 12 β€” change the font size, overriding $self->{' fontsize'}. Example: 12 pt font.
  • 'wordspace' => 0.8 β€” change the additional word spacing, overriding $self->wordspace(). Example: add 0.8 pt between words.
  • 'charspace' => -2.1 β€” change the additional character spacing, overriding $self->charspace(). Example: subtract 2.1 pt between letters, to condense the text.
  • 'hscale' => 125 β€” change the horizontal scaling factor, overriding $self->hscale(). Example: stretch text to 125% of its natural width.

Returns the width of the $string (when set as a line of type), based on all currently set text-state attributes. These can optionally be overridden with %opts. Important detail bestie: these values temporarily replace the existing values, not scaling them up or down. For example, if the existing charspace is 2, and you give in options a value of 3, the value used is 3, not 5. 🫢

Note: this does not affect the PDF in any way. It only tells you how much horizontal space a text string will take up.

Alternate name: "text_width".

This is provided for compatibility with PDF::API2.


🌸 Rendering Text 🌸

✍️ Single Lines ✍️

text

$width = $content->text($text, %opts)

Adds text to the page (left justified by default). The width used (in points) is returned. so useful, so iconic.

Options:

  • 'align' => position β€” align the text, assuming left-to-right writing direction (RTL/bidirectional is not currently supported).
    • 'l' or 'left' (case insensitive). β€” default. Text begins at the current text position.
    • 'c' or 'center' (case insensitive). β€” Text is centered at the current text position.
    • 'r' or 'right' (case insensitive). β€” Text ends (is right justified to) at the current text position.

In all cases, the ending text position is at the (right) end of the text. If mixing various alignments, you should explicitly place the current text position so as to not overwrite earlier text. πŸ’€

  • 'indent' => $distance β€” indents the text by the number of points (a value less than 0 gives an outdent). The indentation amount moves the text left (negative indentation) or right (positive indentation), regardless of alignment. This allows desired alignment effects (for centered and right) that aren't exactly aligned on the current position. For example, consider a column of decimal numbers centered on a desired x position, but aligned on their decimal points. The "indent" would be on a per-line basis, adjusted by the length of the number and the decimal position. math girlies rise πŸ“
  • 'underline' => 'none'
  • 'underline' => 'auto'
  • 'underline' => $distance
  • 'underline' => [$distance, $thickness, ...] β€” underlines the text. $distance is the number of units beneath the baseline, and $thickness is the width of the line. Multiple underlines can be made by passing several distances and thicknesses. A value of 'none' means no underlining (is the default).

Example:

# 3 underlines:
#   distance 4, thickness 1, color red
#   distance 7, thickness 1.5, color yellow
#   distance 11, thickness 2, color (strokecolor default)
'underline' => [4,[1,'red'],7,[1.5,'yellow'],11,2],
  • 'strikethru' => 'none'
  • 'strikethru' => 'auto'
  • 'strikethru' => $distance
  • 'strikethru' => [$distance, $thickness, ...] β€” strikes through the text (like HTML s tag). A value of 'auto' places the line about 30% of the font size above the baseline, or a specified $distance (above the baseline) and $thickness (in points). Multiple strikethroughs can be made by passing several distances and thicknesses. A value of 'none' means no strikethrough. It is the default.

Example:

# 2 strikethroughs:
#   distance 4, thickness 1, color red
#   distance 7, thickness 1.5, color yellow
'strikethru' => [4,[1,'red'],7,[1.5,'yellow']],
  • 'strokecolor' => color_spec β€” defines the underline or strikethru line color, if different from the text color.

textHS

$width = $content->textHS($HSarray, $settings, %opts)

Takes an array of hashes produced by HarfBuzz::Shaper and outputs them to the PDF output file. HarfBuzz outputs glyph CIDs and positioning information. It may rearrange and swap characters (glyphs), and the result may bear no resemblance to the original Unicode point list. it's giving chaos but make it typography 😭

You should see examples/HarfBuzz.pl, which shows a number of examples with Latin and non-Latin text, as well as vertical writing. https://www.catskilltech.com/Examples has a sample available in case you want to see some examples of what HarfBuzz can do, and don't yet have HarfBuzz::Shaper installed. πŸ¦‹

  • $HSarray β€” this is the reference to array of hashes produced by HarfBuzz::Shaper, normally unchanged after being created (but can be modified). See "Using Shaper" in PDF::Builder::Docs for some things that can be done. ✨
  • $settings β€” this a reference to a hash of various pieces of information that textHS() needs in order to function. They include:
    • 'script' => 'script_name' β€” this is the standard 4 letter code (e.g., 'Latn') for the script (alphabet and writing system) you're using. Currently, only Latn (Western writing systems) do kerning, and 'Latn' is the default. HarfBuzz::Shaper will usually be able to figure out from the Unicode points used what the script is, and you might be able to use the set_script() call to override its guess. However, PDF::Builder and HarfBuzz::Shaper do not talk to each other about the script being used. they're not texting, they're not in the group chat, nothing πŸ™ˆ
  • 'features' => array_of_features β€” bestie this one is required, okay? but it can literally be empty, like $settings->{'features'} = ();. You can toss in switches using the standard HarfBuzz naming, plus a + or - switch β€” so -liga turns off ligatures, for example. Right now the only features supported are -liga and -kern (turn off ligatures, turn off ligatures and kerning). Heads up though: this is a whole separate thing from any feature switches you send to HarfBuzz::Shaper (via $hb->add_features(), etc.) when you actually run it, i.e. before textHS(). πŸ’…
  • 'language' => 'language_code' β€” optional, and ngl, right now it doesn't seem to do anything substantial with HarfBuzz::Shaper. It's the standard language code, like 'en' or 'en_US'. You might need to set it for HarfBuzz::Shaper in case that system can't figure out the language rules on its own. πŸ¦‹
  • 'dir' => 'flag' β€” tells textHS() whether this text gets written Left-To-Right (L, which is the default), Right-To-Left (R), Top-To-Bottom (T), or Bottom-To-Top (B). HarfBuzz::Shaper can usually suss out the direction from the script being used (the Unicode points), but here's the tea: HarfBuzz::Shaper does not share that info with PDF::Builder β€” so you gotta specify the direction yourself, unless you're cool with the default LTR. You can peek at HarfBuzz::Shaper's get_direction() call (plus get_language() and get_script()) to see what HarfBuzz thinks the correct direction is. And set_direction() can override Shaper's guess.

Okay so also, fun fact: if the direction is RTL, HarfBuzz reverses the text and hands you back an array with the last character first (so it can be written LTR). Same energy for BTT β€” HarfBuzz reverses the text and returns a string meant to be written top-down. Languages that are normally written horizontally usually get set vertically with direction TTB. And if you're setting text vertically, ligatures and kerning β€” plus character connectivity for cursive scripts β€” get automatically turned off. So please don't let the direction default to LTR or RTL in the Shaper call and then try to patch it up in textHS(). 😭

  • align => 'flag' β€” given the current output location, align the text at the Beginning of the line (left for LTR, right for RTL), Centered on the location, or at the End of the line (right for LTR, left for RTL). Default is B. Centered is basically the text_center() vibe, and End is basically text_right(). Same kind of alignments happen for TTB and BTT too. ✨
  • 'dump' => flag β€” set it to 1 and it prints positioning + glyph CID info (to STDOUT) for every single glyph in the chunk. Default is 0 (no dump, no drama). 🫢
  • 'minKern' => amount (default 1) β€” if the kerning amount (where the font character width differs from the glyph ax value) is bigger than this many character grid units, it uses the unaltered ax for the width (textHS() will output a kern amount in the TJ operation). Otherwise it ignores kerning and uses ax of the actual character width. The whole point? not bloating your PDF with a million unnecessary tiny kerning adjustments in the TJ operation.

%opts

This is a hash of options.

  • 'underline' => underlining_instructions β€” see text() for the available instructions. πŸ’œ
  • 'strikethru' => strikethrough_instructions β€” see text() for the available instructions. πŸ’œ
  • 'strokecolor' => line_color β€” color spec (like 'green', '#FF3377') for the underline or strikethrough, if you didn't already give it in an array alongside its instructions. 🌈

Text gets sent to HarfBuzz::Shaper separately in 'chunks' ('segments') β€” each one a single script (alphabet), a single direction (LTR, RTL, TTB, or BTT), a single font file, and a single font size. A chunk can be a lot of text, but at present textHS() can only output a single line. So for long lines that need splitting into column-width lines, the best move is probably to take the array of hashes HarfBuzz::Shaper returns and split it into smaller chunks at spaces and other whitespace. You may have to query the font to see what the glyph CIDs are for space and anything else you're using. πŸ‘‘

When textHS() is called, it's expected that the font and font size have already been set in PDF::Builder code β€” because that info is needed to interpret what HarfBuzz::Shaper is returning, and to write it to the PDF file. Needless to say, the font should be opened from the same file you gave HarfBuzz::Shaper (ttfont() only, with .ttf or .otf files), and the font size must match. The right location on the page also has to be set already. πŸ“

advancewidthHS, text_widthHS

$width = $content->advancewidthHS($HSarray, $settings, %opts)

Returns the text chunk width (in points) for a Shaper-defined glyph array. This is the horizontal width for LTR and RTL direction, and the vertical height for TTB and BTT direction. Note: you must define the font and font size before calling advancewidthHS().

$HSarray

The array reference of glyphs created by the HarfBuzz::Shaper call. See textHS() for details. 🧚

$settings

The hash reference of settings. See textHS() for details.

  • 'dir' => 'L' etc. β€” the direction of the text, so it knows which "advance" value to sum up. πŸ’«

%opts

Options. But unlike advancewidth(), you cannot override the font, font size, etc. that HarfBuzz::Shaper used to calculate the glyph list.

  • 'doKern' => flag (default 1) β€” if 1, cancel minor kerns per the "minKern" setting. This flag should be 0 (false) if -kern was passed to HarfBuzz::Shaper (i.e., don't kern the text). It's treated as 0 if an ax override setting is given. πŸ’…
  • 'minKern' => amount (default 1) β€” if the kerning amount (font character width differs from glyph ax value) is larger than this many character grid units, use the unaltered ax for the width (textHS() will output a kern amount in the TJ operation). Otherwise, ignore kerning and use ax of the actual character width. The intent is to avoid bloating the PDF code with unnecessary tiny kerning adjustments in the TJ operation. 🌸

Returns total width in points.

Alternate name: "text_widthHS" ✨


πŸŽ€ Advanced Methods πŸŽ€

save

$content->save()

Saves the current graphics state onto a PDF stack. See PDF definition 8.4.2 through 8.4.4 for details. That includes the line width, line cap style, line join style, miter limit, line dash pattern, stroke color, fill color, current transformation matrix, current clipping port, flatness, and dictname. πŸ’Ύ

This method applies to only gfx/graphics objects. If you try it with text objects, you'll get a one-time (per run) warning message, and you should update your code to not do save() and restore() on a text object. Only save() generates the message, since presumably every restore() already had a save() done. πŸ™ˆ

restore

$content->restore()

Restores the most recently saved graphics state (see "save"), removing it from the stack. You can't restore the graphics state (pop it off the stack) unless you've done at least one save (pushed it on the stack). This method applies to both text and gfx/graphics objects. 🫢

add

$content->add(@content)

Adds raw content (arbitrary string(s)) to the PDF stream. You'll generally want to use the other methods in this class instead β€” unless it's to implement some PDF operation that PDF::Builder doesn't natively support. You can hand it an array of multiple strings; they'll be concatenated with spaces between them. πŸ’œ

Be careful when doing this, because you are dabbling in the black arts, directly setting PDF operations! 😭

One interesting use is splitting up an overly long object stream that's giving your editor grief when exploring a PDF file. Just add("\n") every few hundred bytes of output or so to do it. Note that you must use double quotes (quotation marks), not single quotes (apostrophes). ✨

Use extreme care if inserting BT and ET markers into the PDF stream. You may want to use textstart() and textend() calls instead β€” and even then, there are many side effects either way. It's generally not useful to suspend text mode with ET/textend() and BT/textstart(), but it is possible, if you really need to. πŸ”₯

Another useful case: when your input PDF comes from the Chrome browser printing a page to PDF with headers and/or footers. In some versions, this leaves the PDF page with a strange scaling (like the page height in points divided by 3300) and the Y-axis flipped so 0 is at the top. This causes problems when trying to add additional text or graphics in a new text or graphics record, where text is flipped (mirrored) upside down and at the wrong end of the page. If this happens, you might be able to cure it by adding

$scale = .23999999; # example, 792/3300, examine PDF or experiment!
 ...
if ($scale != 1) {
    my @pageDim = $page->mediabox();     # e.g., 0 0 612 792
    my $size_page = $pageDim[3]/$scale;  # 3300 = 792/.23999999
    my $invScale = 1.0/$scale;           # 4.16666684
    $text->add("$invScale 0 0 -$invScale 0 $size_page cm");
}

as the first output to the $text stream. Unfortunately, it's difficult to predict exactly what $scale should be, since it might be 3300 units per page, or a fixed amount. You may need to examine an uncompressed PDF file stream to see what's being used. It might be possible to get the input (original) PDF into a string and look for a certain pattern of "cm" output

.2399999 0 0 -.23999999 0 792 cm

or similar, which is not within a save/restore (q/Q). If the stream is already compressed, this might not be possible. 😬 lowkey a limitation, but ok.


✨ addNS ✨

$content->addNS(@content)

It's giving add()'s chaotic twin fr fr. It works like add(), but it does not make sure there's a space between each element, or before and after the new content. 🫠

Bestie, this means it is entirely on you to make sure any necessary spaces in the PDF stream are placed there explicitly! No hand-holding, no auto-spacing, no vibes-based rescue. πŸ’…

✨ compressFlate ✨

$content->compressFlate()

Marks content for compression on output. πŸ—œοΈβœ¨

This is done automatically in nearly all cases, so you shouldn't need to call this yourself. It just happens, like your fave song coming on shuffle. πŸŽ€

The new() call can set the compress parameter to 'flate' (default) to compress all object streams, or 'none' to suppress compression and allow you to examine the output in an editor. πŸ‘€πŸ“ So if you're the type to go poking around in a text editor, 'none' is your girl.

✨ textstart ✨

$content->textstart()

Starts a text object (ignored if already in a text object β€” it's not gonna double up, she has boundaries πŸ’…).

You will likely want to use the text() method (text context, not text output) instead. iykyk β€” yes, those are two different things, don't @ me.

Note that calling this method, besides outputting a BT marker, will reset most text settings to their default values. In addition, BT itself will reset some transformation matrices. 😭 so yeah, heads up: she resets things.

✨ textend ✨

$content->textend()

Ends a text object (ignored if not in a text object β€” nothing to end, no drama, she simply moves on 🌸).

Note that calling this method, besides outputting an ET marker, will output any accumulated poststream content. 🫢 so it's not just a goodbye, she's also dumping the poststream content on the way out. main character exit energy.

And that's a wrap on this whole page, bestie!! πŸŽ€βœ¨ You just read the entirety of PDF::Bu...:Content(3) from the User Contributed Perl Documentation, and honestly? iconic. This edition came to you courtesy of perl v5.42.0, dated 2026-01-17 β€” go forth and build your PDFs, you absolute legend. πŸ’–πŸ¦‹

πŸ“¦ where this page comes from