Using the Canvas
In this tutorial you will get to know the HexaPDF::Content::Canvas class which is the low-level class for drawing on a page. You might want to open the resulting PDF to more easily understand the operations.
Basics
The canvas class provides access to all PDF drawing instructions. The available instructions are
quite similar to other canvas implementations, like the HTML <canvas>
element. So you can style
and draw graphics as well as text.
The main difference between e.g. the HTML canvas and the PDF canvas is that the PDF canvas methods result in a stream of text instructions that define the content of a PDF page. Whereas the result of HTML canvas methods is an image. This means that one could write the text instructions themselves instead of using the canvas methods. However, since certain rules have to be followed, it is not advised to do so.
With that out of the way, let’s draw some things!
Creating a Canvas
An instance of the canvas class is always tied to one of two PDF objects: a page or a Form XObject. The reason is that the content needs to be associated with such an object to be usable/displayable and that sometimes during drawing operations PDF objects need to be created and correctly assigned.
Therefore the two type classes provide a #canvas
method for getting the appropriate canvas
instance:
require 'hexapdf'
doc = HexaPDF::Document.new
page = doc.pages.add
canvas = page.canvas
form = doc.add({ Type: :XObject, Subtype: :Form, BBox: [0, 0, 100, 100]})
form_canvas = form.canvas
Note that we need to define the bounding box for the Form XObject. The bounding box of a page is its crop box which defaults to the page size by default (which is A4 by default). Anything drawn outside the bounding box/crop box will not be visible.
To access the bounding box of the underlying PDF object we can use the
#box
method:
page_box = canvas.context.box
Coordinate System
The coordinate system of the canvas follows the mathematical model. This means that the positive x-axis is to the right and the positive y-axis to the top. The origin is at (0, 0) which usually coincides with the lower-left corner of the bounding box (but doesn’t need to). Coordinates are specified in PDF points where 72 PDF points are one inch (= 25.4mm).
It is possible to transform the coordinate system using various methods, like the general
#transform
method or the more specific
#translate
method.
Let’s move the origin to somewhere else and draw a rectangle inside which we will draw things:
canvas.translate(100, 100)
canvas.rectangle(0, 0, page_box.width - 200, page_box.height - 200, radius: 15).stroke
Since we translated the origin to the point (100, 100), the rectangle’s lower-left corner will actually be at (100, 100) instead of (0, 0).
Graphics State
The canvas (and also later the PDF application rendering the instructions) keeps track of the so called graphics state.
The graphics state contains properties like the fill and stroke colors, the line width, the font, the character spacing and the current transformation matrix. It is accessible via the #graphics_state method of the canvas.
Let’s use the current transformation matrix of the graphics state to find out the real position of the current origin:
# Note that one would normally use canvas.pos(0, 0) for this
p canvas.graphics_state.ctm.evaluate(0, 0) # prints [100, 100]
Since some changes to the graphics state can’t be undone, like certain coordinate system transformations or clipping operations, it is possible to save and restore the graphics state:
canvas.save_graphics_state do
canvas.rectangle(-50, -50, 100, 100).clip_path.end_path # restricts drawing operations
canvas.fill_color("lightgreen").opacity(fill_alpha: 0.5).
rectangle(-100, -100, 1000, 1000).fill
end
The above instructions fill a rectangle of size 100x100 instead of 1000x1000. If we hadn’t saved the graphics state before and restored it afterwards, we wouldn’t be able to draw anywhere else from this point onwards.
Drawing Graphics
As already said the canvas class provides methods for all PDF drawing instructions. Since those are only low-level constructs there are also some methods that can draw more common graphics like circles.
Let’s draw some basic shapes. We start at the top left, so the y-coordinates are initially high and will get lower:
canvas.fill_color("blue").stroke_color("red").
rectangle(20, 550, 100, 30).fill_stroke
canvas.line_width(10).
move_to(150, 550).curve_to(200, 630, p1: [100, 610]).
line_to(220, 550).close_subpath.stroke
canvas.polygon(300, 550, 300, 630, 360, 590, 270, 580, radius: 10).stroke
Note that we first change parts of the graphics state, setting the fill color to blue and the stroke color to red. Although it might seem that this change is restricted to the rectangle, it is actually not. The same happens with the line width which stays changed for the rounded polygon.
To restrict graphics state changes to only a part of the drawing operations, either use the block form of the methods or explicitly save and restore the graphics state:
canvas.line_width(1) do
canvas.line(10, 520, 200, 520).stroke
end
canvas.save_graphics_state do
canvas.stroke_color("green").line(200, 520, 370, 520).stroke
end
The canvas also features an extension system which allows one to easily draw more complex things, like a pie chart:
pie = canvas.graphic_object(:solid_arc, cx: 100, cy: 450,
outer_a: 50, outer_b: 50)
canvas.fill_color('aaaaff')
canvas.draw(pie, start_angle: 30, end_angle: 110).fill
canvas.fill_color('ffaaaa')
canvas.draw(pie, start_angle: 110, end_angle: 130).fill
canvas.fill_color('aaffaa')
canvas.draw(pie, start_angle: 130, end_angle: 30).fill
Drawing Text
Before any text can be drawn, one has to define at least the font and font size since there is no default value for both values.
PDF has built-in support for the fonts Helvetica, Times and Courier (all tree in normal, bold, italic and bold-italic variants) as well as Symbol and Zapf Dingbats (which are symbolic fonts). Those are called the 14 standard PDF fonts. Note that those fonts only contain a limited set of glyphs, see the standard PDF fonts example for a full list.
Let’s write some text in all standard non-symbolic PDF fonts:
canvas.fill_color("black")
['Helvetica', 'Times', 'Courier'].each_with_index do |font, x|
[:none, :bold, :italic, :bold_italic].each_with_index do |variant, y|
canvas.font(font, variant: variant, size: 12)
canvas.text('HexaPDF Canvas', at: [20 + 120 * x, 360 - 20 * y])
end
end
Besides the standard PDF fonts it is also possible to use any TrueType font with HexaPDF:
canvas.font('/usr/share/fonts/truetype/lato/Lato-Regular.ttf', size: 12)
canvas.text("HexaPDF Canvas with\nTrueType font", at: [20, 270])
Either specify the font file directly, as above, or configure and reference it by name later:
doc.config['font.map'] = {
'Lato' => {
none: '/usr/share/fonts/truetype/lato/Lato-Regular.ttf',
italic: '/usr/share/fonts/truetype/lato/Lato-Italic.ttf',
}
}
canvas.font('Lato', variant: :italic)
canvas.text("Lato Italic font variant", at: [200, 270])
The #text method used here for drawing recognizes line breaks but nothing more. For more complex text output one would need to use HexaPDF::Layout::TextLayouter (low-level) or HexaPDF::Composer (high-level)a.
Reusing Graphics
At the beginning we introduced Form XObjects as the other PDF object type supporting a canvas. Form XObjects are used to define graphics once and use them multiple times.
One thing to remember is that Form XObjects do not reset the graphics state. So all the graphics state set when drawing a Form XObject will be used for the drawing operations of the Form XObject.
Let’s draw something on our Form XObject and then draw it twice:
form_canvas.rectangle(10, 10, 80, 80, radius: 10).stroke
form_canvas.ellipse(0, 0, a: 40, b: 25, inclination: 30).fill_stroke
canvas.xobject(form, at: [20, 120])
canvas.line_width(5).stroke_color("black").opacity(stroke_alpha: 0.5).
fill_color("blue")
canvas.xobject(form, at: [220, 120])
As expected anything outside of the form’s bounding box is clipped and not visible when drawing the form. And it can clearly be seen that the change in the graphics state between the invocations influences the form’s drawing operations.
Now we just need to write the document
doc.write("canvas-tutorial.pdf")
which will conclude this tutorial!
The Complete Code and Result PDF
Here is the complete code generating this result PDF:
require 'hexapdf'
doc = HexaPDF::Document.new
page = doc.pages.add
canvas = page.canvas
form = doc.add({ Type: :XObject, Subtype: :Form, BBox: [0, 0, 100, 100]})
form_canvas = form.canvas
page_box = canvas.context.box
canvas.translate(100, 100)
canvas.rectangle(0, 0, page_box.width - 200, page_box.height - 200, radius: 15).stroke
# Note that one would normally use canvas.pos(0, 0) for this
p canvas.graphics_state.ctm.evaluate(0, 0) # prints [100, 100]
canvas.save_graphics_state do
canvas.rectangle(-50, -50, 100, 100).clip_path.end_path # restricts drawing operations
canvas.fill_color("lightgreen").opacity(fill_alpha: 0.5).
rectangle(-100, -100, 1000, 1000).fill
end
canvas.fill_color("blue").stroke_color("red").
rectangle(20, 550, 100, 30).fill_stroke
canvas.line_width(10).
move_to(150, 550).curve_to(200, 630, p1: [100, 610]).
line_to(220, 550).close_subpath.stroke
canvas.polygon(300, 550, 300, 630, 360, 590, 270, 580, radius: 10).stroke
canvas.line_width(1) do
canvas.line(10, 520, 200, 520).stroke
end
canvas.save_graphics_state do
canvas.stroke_color("green").line(200, 520, 370, 520).stroke
end
pie = canvas.graphic_object(:solid_arc, cx: 100, cy: 450,
outer_a: 50, outer_b: 50)
canvas.fill_color('aaaaff')
canvas.draw(pie, start_angle: 30, end_angle: 110).fill
canvas.fill_color('ffaaaa')
canvas.draw(pie, start_angle: 110, end_angle: 130).fill
canvas.fill_color('aaffaa')
canvas.draw(pie, start_angle: 130, end_angle: 30).fill
canvas.fill_color("black")
['Helvetica', 'Times', 'Courier'].each_with_index do |font, x|
[:none, :bold, :italic, :bold_italic].each_with_index do |variant, y|
canvas.font(font, variant: variant, size: 12)
canvas.text('HexaPDF Canvas', at: [20 + 120 * x, 360 - 20 * y])
end
end
canvas.font('/usr/share/fonts/truetype/lato/Lato-Regular.ttf', size: 12)
canvas.text("HexaPDF Canvas with\nTrueType font", at: [20, 270])
doc.config['font.map'] = {
'Lato' => {
none: '/usr/share/fonts/truetype/lato/Lato-Regular.ttf',
italic: '/usr/share/fonts/truetype/lato/Lato-Italic.ttf',
}
}
canvas.font('Lato', variant: :italic)
canvas.text("Lato Italic font variant", at: [200, 270])
form_canvas.rectangle(10, 10, 80, 80, radius: 10).stroke
form_canvas.ellipse(0, 0, a: 40, b: 25, inclination: 30).fill_stroke
canvas.xobject(form, at: [20, 120])
canvas.line_width(5).stroke_color("black").opacity(stroke_alpha: 0.5).
fill_color("blue")
canvas.xobject(form, at: [220, 120])
doc.write("canvas-tutorial.pdf")