Graphics and images

Draw vector paths, scope graphics state, and embed PNG and JPEG images.

Start with a document and a page. The examples below share these variables:

require "okab"

document = Okab::Document.new(title: "Graphics")
page = document.page(width: 595, height: 842)

Coordinates and line widths use PDF points. RGB colors contain three values between 0 and 1. See getting started for the lower-left coordinate system.

On this page

Draw and paint paths

Path commands build geometry; a paint command fills or strokes it. Rectangles, ellipses, lines, and cubic Bézier curves are supported:

page.rect(48, 650, 180, 80).fill([0.1, 0.4, 0.7])
page.ellipse(280, 650, 120, 80).stroke([0.1, 0.4, 0.7], width: 2)
page.move_to(48, 620).line_to(400, 620)
  .stroke([0.2, 0.2, 0.2], width: 1, dash: [6, 3])

path = Okab::Path.new.move_to(48, 560)
  .curve_to(100, 640, 180, 480, 240, 560)
  .line_to(240, 520).line_to(48, 520).close
page.path(path).fill([0.3, 0.6, 0.8])

Painting consumes the current path. Build it again to paint it a second time, or use fill_and_stroke(color, width: 1) to fill and stroke with the same color. fill accepts rule: :nonzero (default) or :evenodd for overlapping contours.

stroke supports:

Option Values Default
width Positive number in points 1
cap :butt, :round, :square :butt
join :miter, :round, :bevel :miter
miter Positive miter limit 10
dash Array of positive lengths, or nil for a solid line nil

Clip drawing to a shape

with_clip confines drawing to an Okab::Path for the duration of the block and restores the previous graphics state afterward:

clip = Okab::Path.new.ellipse(48, 380, 180, 100)
page.with_clip(clip) do |p|
  p.rect(48, 380, 180, 100).fill([0.1, 0.4, 0.7])
  p.rect(48, 380, 90, 100).fill([0.2, 0.7, 0.6])
end

Use rule: :evenodd for an even-odd clip. Prefer with_clip for scoped drawing; the lower-level clip { |path| ... } API keeps the clip active until the end of the page.

Translate, scale, and rotate

transform(a, b, c, d, e, f) applies the PDF affine matrix within a block: x' = a*x + c*y + e, y' = b*x + d*y + f.

# Move the local origin to (300, 380).
page.transform(1, 0, 0, 1, 300, 380) do |p|
  p.rect(0, 0, 100, 100).stroke([0.1, 0.4, 0.7], width: 2)
end

# Rotate 30 degrees counterclockwise around the local origin.
angle = Math::PI / 6
page.transform(Math.cos(angle), Math.sin(angle),
  -Math.sin(angle), Math.cos(angle), 100, 240) do |p|
  p.rect(0, 0, 120, 40).fill([0.2, 0.7, 0.6])
end

Transforms affect drawing, including text and images. Link rectangles are annotations and still need coordinates in the page’s original coordinate system. The previous graphics state is restored when the block finishes.

Apply opacity

Opacity ranges from 0 (transparent) to 1 (opaque) and applies to drawing within its block:

page.opacity(0.5) do |p|
  p.rect(280, 240, 100, 60).fill([0.1, 0.4, 0.7])
end

Blocks can nest with transforms and clipping. Opacity sets the current value; nested values do not multiply automatically.

Embed images

Pass binary PNG or JPEG bytes to Page#image. The format is detected by default; format: :png or format: :jpeg selects it explicitly:

image = Okab::Image.decode(File.binread("photo.jpg"))
width = 180
height = width * image.height.to_f / image.width
page.image(image, x: 48, y: 48, width: width, height: height)

Set both output dimensions. Okab stretches the image to that rectangle; the calculation above preserves its aspect ratio. PNG alpha is embedded as a soft mask. JPEG compressed bytes are embedded without re-encoding.

Reuse a decoded Okab::Image when placing the same image repeatedly to reuse the document’s image resource. Repeatedly passing raw bytes decodes separate image objects. Images do not gain searchable text or OCR.

Write the result with document.write("graphics.pdf"). See image format limits for accepted PNG and JPEG inputs.