Graphics Primitives

This example shows many of the operations that the canvas implementation allows.

Note that the PDF canvas has its origin in the bottom left corner of the page. This means the coordinate (100, 50) is 100 PDF points from the left side and 50 PDF points from the bottom. One PDF point is equal to 1/72 inch.

Usage:
ruby graphics.rb
Resulting PDF:
graphics.pdf
Preview:

Code

require 'hexapdf'

doc = HexaPDF::Document.new
page = doc.pages.add
canvas = page.canvas

# Draws the shape that is used to showcase the transformations in the given
# color.
def transformation_shape(canvas, *color)
  canvas.stroke_color(*color)
  canvas.polygon(0, 0, 0, 80, 30, 50, 60, 80, 60, 0, 30, 30)
  canvas.line(-30, 0, 30, 0)
  canvas.line(0, 30, 0, -30)
  canvas.stroke
end

# Basic transformations: translate, scale, rotate, skew
canvas.translate(0, 710) do
  normal_color = [0.7, 0.7, 0.3]
  transformed_color = [0.3, 0.7, 0.7]

  canvas.translate(50, 0) do
    transformation_shape(canvas, normal_color)
    canvas.translate(40, 40) { transformation_shape(canvas, transformed_color) }
  end

  canvas.translate(180, 0) do
    transformation_shape(canvas, normal_color)
    canvas.scale(1.7, 1.3) { transformation_shape(canvas, transformed_color) }
  end

  canvas.translate(330, 0) do
    transformation_shape(canvas, normal_color)
    canvas.rotate(30) { transformation_shape(canvas, transformed_color) }
  end

  canvas.translate(430, 0) do
    transformation_shape(canvas, normal_color)
    canvas.skew(15, 30) { transformation_shape(canvas, transformed_color) }
  end
end

# Draws a thin white line over a thick black line.
def dual_lines(canvas)
  canvas.stroke_color(0)
  canvas.line_width = 15
  yield
  canvas.stroke
  canvas.stroke_color(1.0)
  canvas.line_width = 1
  yield
  canvas.stroke
end

# Graphics state: line width, line cap style, line join style, miter limit,
# line dash pattern
canvas.translate(0, 550) do
  canvas.translate(50, 0) do
    [1, 5, 10, 15].each_with_index do |i, index|
      canvas.stroke_color(0)
      canvas.line_width(i)
      canvas.line(20 * index, 0, 20 * index, 100)
      canvas.stroke
    end
  end

  canvas.translate(150, 0) do
    0.upto(2) do |i|
      canvas.line_cap_style = i
      dual_lines(canvas) { canvas.line(20 * i, 0, 20 * i, 100) }
    end
  end

  canvas.translate(230, 0) do
    0.upto(2) do |i|
      canvas.line_join_style = i
      dual_lines(canvas) { canvas.polyline(0, 30 * i, 40, 50 + 30 * i, 80, 30 * i) }
    end
  end

  canvas.translate(350, 0) do
    canvas.line_join_style = :miter
    canvas.miter_limit = 1
    dual_lines(canvas) { canvas.polyline(0, 0, 20, 80, 40, 0) }
    canvas.miter_limit = 10
    dual_lines(canvas) { canvas.polyline(60, 0, 80, 80, 100, 0) }
  end

  canvas.translate(490, 0) do
    canvas.line_width(1)
    [[[1, 1]],
     [[3, 1]],
     [[3, 3]],
     [[5, 1, 1, 1, 1, 1]],
     [[3, 5], 6]].each_with_index do |(value, phase), index|
      canvas.line_dash_pattern(value, phase || 0)
      canvas.line(20 * index, 0, 20 * index, 100)
      canvas.stroke
    end
  end
end

# Basic shapes: line, polyline, (rounded) rectangle, (rounded) polygon, circle, ellipse
canvas.translate(0, 420) do
  canvas.line(50, 0, 50, 100)
  canvas.polyline(80, 0, 80, 20, 70, 30, 90, 40, 70, 50, 90, 60, 70, 70, 80, 80, 80, 100)
  canvas.rectangle(110, 0, 50, 100)
  canvas.rectangle(180, 0, 50, 100, radius: 20)
  canvas.polygon(250, 0, 250, 100, 280, 70, 310, 100, 310, 0, 280, 30)
  canvas.polygon(330, 0, 330, 100, 360, 70, 390, 100, 390, 0, 360, 30, radius: 20)
  canvas.circle(440, 50, 30)
  canvas.ellipse(520, 50, a: 30, b: 15, inclination: 45)
  canvas.stroke
end

# Various arcs w/wo filling, using the Canvas#arc method as well as directly
# working with the arc objects
canvas.translate(0, 320) do
  canvas.arc(50, 50, a: 10, start_angle: -60, end_angle: 115)
  canvas.arc(100, 50, a: 40, b: 20, start_angle: -60, end_angle: 115)
  canvas.arc(180, 50, a: 40, b: 20, start_angle: -60, end_angle: 115, inclination: 45)
  canvas.stroke

  canvas.fill_color(0.4, 0.3, 0.4)
  canvas.arc(250, 50, a: 10, start_angle: -60, end_angle: 115)
  canvas.arc(300, 50, a: 40, b: 20, start_angle: -60, end_angle: 115)
  canvas.arc(380, 50, a: 40, b: 20, start_angle: -60, end_angle: 115, inclination: 45)
  canvas.fill

  arc = canvas.graphic_object(:arc, cx: 450, cy: 50, a: 30, b: 30,
                              start_angle: -30, end_angle: 105)
  canvas.fill_color(0.4, 0.3, 0.4)
  canvas.move_to(450, 50)
  canvas.line_to(*arc.start_point)
  arc.curves.each {|c| canvas.curve_to(*c)}
  canvas.fill
  arc.configure(start_angle: 105, end_angle: -30)
  canvas.fill_color(0.3, 0.7, 0.7)
  canvas.move_to(450, 50)
  canvas.line_to(*arc.start_point)
  arc.curves.each {|c| canvas.curve_to(*c)}
  canvas.fill

  arc = canvas.graphic_object(:arc, cx: 530, cy: 50, a: 40, b: 20,
                              start_angle: -30, end_angle: 105)
  canvas.fill_color(0.4, 0.3, 0.4)
  canvas.move_to(530, 50)
  canvas.line_to(*arc.start_point)
  arc.curves.each {|c| canvas.curve_to(*c)}
  canvas.fill
  arc.configure(start_angle: 105, end_angle: -30)
  canvas.fill_color(0.7, 0.7, 0.3)
  canvas.move_to(530, 50)
  canvas.line_to(*arc.start_point)
  arc.curves.each {|c| canvas.curve_to(*c)}
  canvas.fill
end

# Draws a circle and two half circles inside with different directions.
def shapes_to_paint(canvas)
  canvas.line_width = 2
  canvas.arc(50, 50, a: 50)
  canvas.arc(50, 60, a: 25, end_angle: 180, clockwise: false)
  canvas.arc(50, 40, a: 25, end_angle: 180, clockwise: true)
end

# Draws arrows showing the direction of the #shapes_to_paint
def arrows(canvas)
  canvas.line_width = 1
  canvas.polyline(95, 45, 100, 50, 105, 45)
  canvas.polyline(55, 105, 50, 100, 55, 95)
  canvas.polyline(-5, 55, 0, 50, 5, 55)
  canvas.polyline(45, 5, 50, 0, 45, -5)
  canvas.polyline(55, 90, 50, 85, 55, 80)
  canvas.polyline(55, 20, 50, 15, 55, 10)
  canvas.stroke
end

# Path painting and clipping operations: stroke, close and stroke, fill nonzero,
# fill even-odd, fill nonzero and stroke, fill even-odd and stroke, close and
# fill nonzero and stroke, close fill even-odd and stroke, clip even-odd, clip
# nonzero
canvas.translate(0, 190) do
  canvas.fill_color(0.3, 0.7, 0.7)

  [
    [:stroke], [:close_stroke], [:fill, :nonzero], [:fill, :even_odd],
    [:fill_stroke, :nonzero], [:fill_stroke, :even_odd],
    [:close_fill_stroke, :nonzero], [:close_fill_stroke, :even_odd]
  ].each_with_index do |op, index|
    row = (1 - (index / 4))
    column = index % 4
    x = 50 + 80 * column
    y = 80 * row
    canvas.transform(0.6, 0, 0, 0.6, x, y) do
      shapes_to_paint(canvas)
      canvas.send(*op)
      arrows(canvas)
    end
  end

  canvas.fill_color(0.7, 0.7, 0.3)

  [:even_odd, :nonzero].each_with_index do |op, index|
    canvas.translate(370 + 110 * index, 20) do
      canvas.circle(50, 50, 50)
      canvas.circle(50, 50, 20)
      canvas.clip_path(op)
      canvas.end_path
      canvas.rectangle(0, 0, 100, 100, radius: 100)
      canvas.fill_stroke
    end
  end
end

# Some composite shapes, an image and a form XObject
canvas.translate(0, 80) do
  canvas.fill_color(0.3, 0.7, 0.7)
  canvas.rectangle(50, 0, 80, 80, radius: 80)
  canvas.fill

  solid = canvas.graphic_object(:solid_arc, cx: 190, cy: 40, inner_a: 20, inner_b: 15,
                                outer_a: 40, outer_b: 30, start_angle: 10, end_angle: 130)

  canvas.line_width(0.5)
  canvas.opacity(fill_alpha: 0.5, stroke_alpha: 0.2) do
    canvas.fill_color('AA8888').draw(solid).fill_stroke
    canvas.fill_color('88AA88').draw(solid, start_angle: 130, end_angle: 220).fill_stroke
    canvas.fill_color('8888AA').draw(solid, start_angle: 220, end_angle: 10).fill_stroke

    solid.configure(inner_a: 0, inner_b: 0, outer_a: 40, outer_b: 40, cx: 290)
    canvas.fill_color('AA8888').draw(solid, start_angle: 10, end_angle: 130).fill_stroke
    canvas.fill_color('88AA88').draw(solid, start_angle: 130, end_angle: 220).fill_stroke
    canvas.fill_color('8888AA').draw(solid, start_angle: 220, end_angle: 10).fill_stroke

    canvas.image(File.join(__dir__, 'machupicchu.jpg'), at: [350, 0], height: 80)
  end
end

# A simple rainbow color band
canvas.translate(0, 20) do
  canvas.line_width = 6
  freq = 0.1
  0.upto(100) do |i|
    r = Math.sin(freq * i) * 127 + 128
    g = Math.sin(freq * i + 2) * 127 + 128
    b = Math.sin(freq * i + 4) * 127 + 128
    canvas.stroke_color(r.to_i, g.to_i, b.to_i)
    canvas.line(50 + i * 5, 0, 50 + i * 5, 40)
    canvas.stroke
  end
end

# Reusing the already draw graphics for an XObject
# Note that converting the page to a form XObject automatically closes all open
# graphics states, therefore this can't be inside the above Canvas#translate
# call
form = doc.add(page.to_form_xobject(reference: false))
canvas.rectangle(480, 80, form.box.width * (100 / form.box.height.to_f), 100).stroke
canvas.xobject(form, at: [480, 80], height: 100)

doc.write('graphics.pdf', optimize: true)