Class: Pdfrb::Content::Canvas

Inherits:
Object
  • Object
show all
Defined in:
lib/pdfrb/content/canvas.rb

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(stream, document: nil) ⇒ Canvas

Returns a new instance of Canvas.



8
9
10
11
12
13
14
15
16
# File 'lib/pdfrb/content/canvas.rb', line 8

def initialize(stream, document: nil)
  Pdfrb::Content::Operator.eager_load!
  @stream = stream
  @document = document || stream.document
  @serializer = Pdfrb::Serializer.new
  @used_fonts = {}
  @used_xobjects = {}
  ensure_stream_payload
end

Instance Attribute Details

#documentObject (readonly)

Returns the value of attribute document.



6
7
8
# File 'lib/pdfrb/content/canvas.rb', line 6

def document
  @document
end

#serializerObject (readonly)

Returns the value of attribute serializer.



6
7
8
# File 'lib/pdfrb/content/canvas.rb', line 6

def serializer
  @serializer
end

#streamObject (readonly)

Returns the value of attribute stream.



6
7
8
# File 'lib/pdfrb/content/canvas.rb', line 6

def stream
  @stream
end

#used_fontsObject (readonly)

Returns the value of attribute used_fonts.



6
7
8
# File 'lib/pdfrb/content/canvas.rb', line 6

def used_fonts
  @used_fonts
end

#used_xobjectsObject (readonly)

Returns the value of attribute used_xobjects.



6
7
8
# File 'lib/pdfrb/content/canvas.rb', line 6

def used_xobjects
  @used_xobjects
end

Instance Method Details

#arc(cx, cy, radius, start_angle:, end_angle:, segments_per_rev: 16) ⇒ Object

Approximate a circular arc from start_angle to end_angle (radians) centered at (cx, cy) with radius. Uses 16-line segment approximation per full revolution.



93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
# File 'lib/pdfrb/content/canvas.rb', line 93

def arc(cx, cy, radius, start_angle:, end_angle:, segments_per_rev: 16)
  span = end_angle - start_angle
  return self if span.abs < 1e-6

  seg_count = [(segments_per_rev * span.abs / (2 * Math::PI)).ceil, 1].max
  step = span / seg_count
  seg_count.times do |i|
    a0 = start_angle + (i * step)
    a1 = a0 + step
    x0 = cx + (radius * Math.cos(a0))
    y0 = cy + (radius * Math.sin(a0))
    x1 = cx + (radius * Math.cos(a1))
    y1 = cy + (radius * Math.sin(a1))
    i.zero? ? move_to(x0, y0) : line_to(x0, y0)
    line_to(x1, y1)
  end
  self
end

#artifact(type = nil) ⇒ Object



451
452
453
454
455
456
457
# File 'lib/pdfrb/content/canvas.rb', line 451

def artifact(type = nil, &)
  if type
    marked_content(:Artifact, { Type: type }, &)
  else
    marked_content(:Artifact, &)
  end
end

#blend_mode=(mode) ⇒ Object



278
279
280
281
# File 'lib/pdfrb/content/canvas.rb', line 278

def blend_mode=(mode)
  emit_op(Pdfrb::Content::Operator::ApplyExtGState,
          create_ext_g_state(BM: mode.to_s))
end

#circle(cx, cy, radius) ⇒ Object

Full circle centered at (cx, cy) with radius.



113
114
115
116
# File 'lib/pdfrb/content/canvas.rb', line 113

def circle(cx, cy, radius)
  arc(cx, cy, radius, start_angle: 0, end_angle: 2 * Math::PI)
  close_path
end

#clipObject



236
237
238
239
240
# File 'lib/pdfrb/content/canvas.rb', line 236

def clip
  emit_op(Pdfrb::Content::Operator::ClipNonZero)
  emit_op(Pdfrb::Content::Operator::EndPath)
  self
end

#clip_even_oddObject



242
243
244
245
246
# File 'lib/pdfrb/content/canvas.rb', line 242

def clip_even_odd
  emit_op(Pdfrb::Content::Operator::ClipEvenOdd)
  emit_op(Pdfrb::Content::Operator::EndPath)
  self
end

#close_pathObject



205
206
207
208
# File 'lib/pdfrb/content/canvas.rb', line 205

def close_path
  emit_op(Pdfrb::Content::Operator::ClosePath)
  self
end

#concat(a, b, c, d, e, f) ⇒ Object



45
46
47
48
49
50
51
52
53
# File 'lib/pdfrb/content/canvas.rb', line 45

def concat(a, b, c, d, e, f, &)
  return emit_op(Pdfrb::Content::Operator::ConcatMatrix, a, b, c, d, e, f) unless block_given?

  save_graphics_state do
    emit_op(Pdfrb::Content::Operator::ConcatMatrix, a, b, c, d, e, f)
    yield self
  end
  self
end

#corner(x0, y0, x1, y1) ⇒ Object

Draw a quarter-circle corner from (x0, y0) to (x1, y1) by multi-segment linear interpolation. Adequate for typical radii (< 30 pts); for larger radii use curve_to directly.



181
182
183
184
185
186
187
# File 'lib/pdfrb/content/canvas.rb', line 181

def corner(x0, y0, x1, y1)
  segments = 6
  segments.times do |i|
    t = (i + 1) / segments.to_f
    line_to(x0 + ((x1 - x0) * t), y0 + ((y1 - y0) * t))
  end
end

#curve_to(c1x, c1y, c2x, c2y, x, y) ⇒ Object



189
190
191
192
# File 'lib/pdfrb/content/canvas.rb', line 189

def curve_to(c1x, c1y, c2x, c2y, x, y)
  emit_op(Pdfrb::Content::Operator::CurveTo, c1x, c1y, c2x, c2y, x, y)
  self
end

#dash=(spec) ⇒ Object

Convenience alias matching HexaPDF's naming. Accepts either an Array (interpreted as the dash array, phase 0) or an [array, phase] pair.



164
165
166
167
168
169
170
171
172
173
174
175
176
# File 'lib/pdfrb/content/canvas.rb', line 164

def dash=(spec)
  array, phase = case spec
                 when ::Array
                   if spec.first.is_a?(::Array)
                     spec
                   else
                     [spec, 0]
                   end
                 else
                   [[spec], 0]
                 end
  emit_op(Pdfrb::Content::Operator::DashPattern, array, phase)
end

#dash_pattern=(spec) ⇒ Object



417
418
419
420
# File 'lib/pdfrb/content/canvas.rb', line 417

def dash_pattern=(spec)
  array, phase = spec.is_a?(::Array) ? spec : [spec, 0]
  emit_op(Pdfrb::Content::Operator::DashPattern, array, phase)
end

#draw_form_xobject(name, at: [0, 0], matrix: nil) ⇒ Object

Draw a Form XObject by resource name at (x, y) with an optional matrix transform. Public API for page stamping, form flattening, and appearance-stream embedding.

Parameters:

  • name (Symbol)

    the resource name in /Resources /XObject.

  • at (Array<Numeric>) (defaults to: [0, 0])

    x, y translation.

  • matrix (Array<Numeric>, nil) (defaults to: nil)

    6-element transform matrix.



314
315
316
317
318
# File 'lib/pdfrb/content/canvas.rb', line 314

def draw_form_xobject(name, at: [0, 0], matrix: nil)
  translate = matrix.nil? ? at : nil
  invoke_xobject(name, matrix: matrix, pre_translate: translate)
  self
end

#draw_image(name, at: nil, width: nil, height: nil, matrix: nil) ⇒ Object



292
293
294
295
296
297
298
299
300
301
# File 'lib/pdfrb/content/canvas.rb', line 292

def draw_image(name, at: nil, width: nil, height: nil, matrix: nil)
  if matrix
    a, b, c, d, e, f = matrix
    invoke_xobject(name, matrix: [a, b, c, d, e, f])
  else
    invoke_xobject(name, matrix: [width, 0, 0, height, 0, 0],
                         pre_translate: at)
  end
  self
end

#draw_image_matrix(name, a:, b:, c:, d:, e:, f:) ⇒ Object



303
304
305
306
# File 'lib/pdfrb/content/canvas.rb', line 303

def draw_image_matrix(name, a:, b:, c:, d:, e:, f:)
  invoke_xobject(name, matrix: [a, b, c, d, e, f])
  self
end

#ellipse(cx, cy, rx, ry) ⇒ Object

Ellipse centered at (cx, cy) with x/y radii (rx, ry). Built from a scaled circle.



120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
# File 'lib/pdfrb/content/canvas.rb', line 120

def ellipse(cx, cy, rx, ry)
  save_graphics_state do
    translate(cx, cy)
    concat(1, 0, 0, 1, 0, 0)
    # Approximate ellipse by scaling a unit circle.
    segments = 32
    segments.times do |i|
      a0 = (i / segments.to_f) * 2 * Math::PI
      a1 = ((i + 1) / segments.to_f) * 2 * Math::PI
      x0 = rx * Math.cos(a0)
      y0 = ry * Math.sin(a0)
      x1 = rx * Math.cos(a1)
      y1 = ry * Math.sin(a1)
      i.zero? ? move_to(x0, y0) : line_to(x0, y0)
      line_to(x1, y1)
    end
    close_path
  end
  self
end

#emit_op(op_class, *operands) ⇒ Object



475
476
477
478
# File 'lib/pdfrb/content/canvas.rb', line 475

def emit_op(op_class, *operands)
  bytes = op_class.serialize(@serializer, *operands)
  append(bytes)
end

#end_marked_contentObject



439
440
441
442
# File 'lib/pdfrb/content/canvas.rb', line 439

def end_marked_content
  emit_op(Pdfrb::Content::Operator::EndMarkedContent)
  self
end

#end_pathObject



231
232
233
234
# File 'lib/pdfrb/content/canvas.rb', line 231

def end_path
  emit_op(Pdfrb::Content::Operator::EndPath)
  self
end

#fill(rule: :nonzero) ⇒ Object



215
216
217
218
219
220
221
# File 'lib/pdfrb/content/canvas.rb', line 215

def fill(rule: :nonzero)
  op = rule == :even_odd ?
         Pdfrb::Content::Operator::FillEvenOdd :
         Pdfrb::Content::Operator::FillNonZero
  emit_op(op)
  self
end

#fill_color(color) ⇒ Object



253
254
255
256
257
258
259
260
261
# File 'lib/pdfrb/content/canvas.rb', line 253

def fill_color(color)
  case color
  in [Symbol | String => family, *rest]
    emit_color_op(true, family.to_sym, rest)
  else
    emit_color_op(true, :gray, [color])
  end
  self
end

#fill_shading(name) ⇒ Object



248
249
250
251
# File 'lib/pdfrb/content/canvas.rb', line 248

def fill_shading(name)
  append(" /#{name} sh\n")
  self
end

#fill_stroke(rule: :nonzero) ⇒ Object



223
224
225
226
227
228
229
# File 'lib/pdfrb/content/canvas.rb', line 223

def fill_stroke(rule: :nonzero)
  op = rule == :even_odd ?
         Pdfrb::Content::Operator::FillStrokeEvenOdd :
         Pdfrb::Content::Operator::FillStrokeNonZero
  emit_op(op)
  self
end

#inline_image(dict:, data:) ⇒ Object

Emit an inline image (BI ... ID ... EI) directly into the content stream. Avoids the /Resources /XObject registration overhead for one-shot images. The dict keys are the image header keys (/W, /H, /CS, /BPC, /F, etc.); data is the encoded image bytes (raw, Flate-compressed, or DCT).

Parameters:

  • dict (Hash{Symbol=>Object})

    inline image header entries (e.g. { W: 100, H: 100, CS: :RGB, BPC: 8, F: :DCTDecode }).

  • data (String)

    raw image bytes (binary-encoded).



344
345
346
347
348
349
350
351
352
353
354
# File 'lib/pdfrb/content/canvas.rb', line 344

def inline_image(dict:, data:)
  header = +"BI\n"
  dict.each do |k, v|
    header << @serializer.serialize(k) << " " << @serializer.serialize(v) << "\n"
  end
  header << "ID\n"
  append(header)
  append(data)
  append("\nEI\n")
  self
end

#invoke_xobject(name, matrix: nil, pre_translate: nil) ⇒ Object

Invoke an XObject (/Do) inside a saved graphics state, applying an optional translation then a 6-element transform matrix.



322
323
324
325
326
327
328
329
330
331
332
333
# File 'lib/pdfrb/content/canvas.rb', line 322

def invoke_xobject(name, matrix: nil, pre_translate: nil)
  @used_xobjects[name] = true
  save_graphics_state do
    translate(pre_translate[0], pre_translate[1]) if pre_translate
    if matrix
      a, b, c, d, e, f = matrix
      emit_op(Pdfrb::Content::Operator::ConcatMatrix, a, b, c, d, e, f)
    end
    emit_op(Pdfrb::Content::Operator::InvokeXObject, name)
  end
  self
end

#line(x1 = nil, y1 = nil, x2 = nil, y2 = nil, from: nil, to: nil) ⇒ Object



65
66
67
68
69
70
71
# File 'lib/pdfrb/content/canvas.rb', line 65

def line(x1 = nil, y1 = nil, x2 = nil, y2 = nil, from: nil, to: nil)
  if from && to
    x1, y1 = from
    x2, y2 = to
  end
  move_to(x1, y1).line_to(x2, y2)
end

#line_cap=(n) ⇒ Object



405
406
407
# File 'lib/pdfrb/content/canvas.rb', line 405

def line_cap=(n)
  emit_op(Pdfrb::Content::Operator::LineCap, n)
end

#line_join=(n) ⇒ Object



409
410
411
# File 'lib/pdfrb/content/canvas.rb', line 409

def line_join=(n)
  emit_op(Pdfrb::Content::Operator::LineJoin, n)
end

#line_to(x, y) ⇒ Object



60
61
62
63
# File 'lib/pdfrb/content/canvas.rb', line 60

def line_to(x, y)
  emit_op(Pdfrb::Content::Operator::LineTo, x, y)
  self
end

#line_width=(n) ⇒ Object



401
402
403
# File 'lib/pdfrb/content/canvas.rb', line 401

def line_width=(n)
  emit_op(Pdfrb::Content::Operator::LineWidth, n)
end

#marked_content(tag, properties = nil) ⇒ Object



422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
# File 'lib/pdfrb/content/canvas.rb', line 422

def marked_content(tag, properties = nil, &)
  if properties
    emit_op(Pdfrb::Content::Operator::BeginMarkedContentWithProperties,
            tag, properties)
  else
    emit_op(Pdfrb::Content::Operator::BeginMarkedContent, tag)
  end
  return self unless block_given?

  begin
    yield self
  ensure
    emit_op(Pdfrb::Content::Operator::EndMarkedContent)
  end
  self
end

#miter_limit=(n) ⇒ Object



413
414
415
# File 'lib/pdfrb/content/canvas.rb', line 413

def miter_limit=(n)
  emit_op(Pdfrb::Content::Operator::MiterLimit, n)
end

#move_to(x, y) ⇒ Object



55
56
57
58
# File 'lib/pdfrb/content/canvas.rb', line 55

def move_to(x, y)
  emit_op(Pdfrb::Content::Operator::MoveTo, x, y)
  self
end

#opacity=(alpha) ⇒ Object



273
274
275
276
# File 'lib/pdfrb/content/canvas.rb', line 273

def opacity=(alpha)
  emit_op(Pdfrb::Content::Operator::ApplyExtGState,
          create_ext_g_state(ca: alpha, CA: alpha))
end

#polygon(points) ⇒ Object

Draw a closed polygon through points.



86
87
88
# File 'lib/pdfrb/content/canvas.rb', line 86

def polygon(points)
  polyline(points, close: true)
end

#polyline(points, close: false) ⇒ Object

Draw a connected polyline through points. Each point is [x, y]. If close is true, append a close-path operator.



75
76
77
78
79
80
81
82
83
# File 'lib/pdfrb/content/canvas.rb', line 75

def polyline(points, close: false)
  return self if points.empty?

  first = points.first
  move_to(first[0], first[1])
  points.drop(1).each { |x, y| line_to(x, y) }
  close_path if close
  self
end

#populate_resources!(page) ⇒ Object



459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
# File 'lib/pdfrb/content/canvas.rb', line 459

def populate_resources!(page)
  r = page.value[:Resources]
  r = {} unless r.is_a?(::Hash)
  unless @used_fonts.empty?
    fd = r[:Font] || {}
    @used_fonts.each_key { |n| fd[n] = fd[n] || n }
    r[:Font] = fd
  end
  unless @used_xobjects.empty?
    xd = r[:XObject] || {}
    @used_xobjects.each_key { |n| xd[n] = xd[n] || n }
    r[:XObject] = xd
  end
  page.value[:Resources] = r
end

#rectangle(x = nil, y = nil, w = nil, h = nil, point: nil, width: nil, height: nil) ⇒ Object

Raises:

  • (ArgumentError)


194
195
196
197
198
199
200
201
202
203
# File 'lib/pdfrb/content/canvas.rb', line 194

def rectangle(x = nil, y = nil, w = nil, h = nil,
              point: nil, width: nil, height: nil)
  x, y = point if point
  w = width if w.nil?
  h = height if h.nil?
  raise ArgumentError, "rectangle needs x, y, width and height" if [x, y, w, h].any?(&:nil?)

  emit_op(Pdfrb::Content::Operator::Rectangle, x, y, w, h)
  self
end

#rotate(angle_in_radians) ⇒ Object



39
40
41
42
43
# File 'lib/pdfrb/content/canvas.rb', line 39

def rotate(angle_in_radians, &)
  c = Math.cos(angle_in_radians)
  s = Math.sin(angle_in_radians)
  concat(c, s, -s, c, 0, 0, &)
end

#rounded_rectangle(x, y, width, height, radius) ⇒ Object

Rectangle with rounded corners. radius is the corner radius (a single value or 4-element [tl, tr, br, bl]).



143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
# File 'lib/pdfrb/content/canvas.rb', line 143

def rounded_rectangle(x, y, width, height, radius)
  radii = radius.is_a?(::Array) ? radius : [radius] * 4
  tl, tr, br, bl = radii
  # bottom edge
  move_to(x + bl, y)
  line_to(x + width - br, y)
  corner(x + width - br, y, x + width, y + br)
  line_to(x + width, y + height - tr)
  corner(x + width, y + height - tr,
         x + width - tr, y + height)
  line_to(x + tl, y + height)
  corner(x + tl, y + height, x, y + height - tl)
  line_to(x, y + bl)
  corner(x, y + bl, x + bl, y)
  close_path
  self
end

#save_graphics_stateObject Also known as: with_graphics_state



18
19
20
21
22
23
24
25
26
27
28
# File 'lib/pdfrb/content/canvas.rb', line 18

def save_graphics_state(&)
  emit_op Pdfrb::Content::Operator::SaveGraphicsState
  if block_given?
    begin
      yield self
    ensure
      emit_op Pdfrb::Content::Operator::RestoreGraphicsState
    end
  end
  self
end

#scale(sx, sy = sx) ⇒ Object



35
36
37
# File 'lib/pdfrb/content/canvas.rb', line 35

def scale(sx, sy = sx, &)
  concat(sx, 0, 0, sy, 0, 0, &)
end

#strokeObject



210
211
212
213
# File 'lib/pdfrb/content/canvas.rb', line 210

def stroke
  emit_op(Pdfrb::Content::Operator::Stroke)
  self
end

#stroke_color(color) ⇒ Object



263
264
265
266
267
268
269
270
271
# File 'lib/pdfrb/content/canvas.rb', line 263

def stroke_color(color)
  case color
  in [Symbol | String => family, *rest]
    emit_color_op(false, family.to_sym, rest)
  else
    emit_color_op(false, :gray, [color])
  end
  self
end

#tagged(tag, mcid: nil, **props) ⇒ Object



444
445
446
447
448
449
# File 'lib/pdfrb/content/canvas.rb', line 444

def tagged(tag, mcid: nil, **props, &)
  p = props.dup
  p[:MCID] = mcid if mcid
  p = nil if p.empty?
  marked_content(tag, p, &)
end

#text(str, at:, font:, size:, char_spacing: nil, word_spacing: nil) ⇒ Object



356
357
358
359
360
361
362
363
364
365
366
367
368
# File 'lib/pdfrb/content/canvas.rb', line 356

def text(str, at:, font:, size:, char_spacing: nil, word_spacing: nil)
  @used_fonts[font] = size
  @document&.fonts&.register_usage(font, str)
  encoded = encode_for_font(str.to_s, font)
  emit_op(Pdfrb::Content::Operator::BeginText)
  emit_op(Pdfrb::Content::Operator::SetTextMatrix, 1, 0, 0, 1, at[0], at[1])
  emit_op(Pdfrb::Content::Operator::Font, font, size)
  emit_op(Pdfrb::Content::Operator::CharSpacing, char_spacing) if char_spacing
  emit_op(Pdfrb::Content::Operator::WordSpacing, word_spacing) if word_spacing
  emit_op(Pdfrb::Content::Operator::ShowText, encoded)
  emit_op(Pdfrb::Content::Operator::EndText)
  self
end

#text_lines(lines, font:, size:, at:, leading: nil, char_spacing: nil, word_spacing: nil) ⇒ Object



370
371
372
373
374
375
376
377
378
379
380
# File 'lib/pdfrb/content/canvas.rb', line 370

def text_lines(lines, font:, size:, at:, leading: nil, char_spacing: nil,
               word_spacing: nil)
  lead = leading || (size * 1.2)
  x, y = at
  lines.each do |line|
    text(line, at: [x, y], font: font, size: size,
               char_spacing: char_spacing, word_spacing: word_spacing)
    y -= lead
  end
  self
end

#text_rich(runs, at:) ⇒ Object



382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/pdfrb/content/canvas.rb', line 382

def text_rich(runs, at:)
  emit_op(Pdfrb::Content::Operator::BeginText)
  cx, cy = at
  runs.each do |run|
    @used_fonts[run[:font]] = run[:size]
    emit_op(Pdfrb::Content::Operator::SetTextMatrix, 1, 0, 0, 1, cx, cy)
    emit_op(Pdfrb::Content::Operator::Font, run[:font], run[:size])
    fill_color(run[:color]) if run[:color]
    emit_op(Pdfrb::Content::Operator::ShowText,
            encode_for_font(run[:text].to_s, run[:font]))
    advance = @document&.fonts&.measure_text(
      run[:text], font: run[:font], size: run[:size]
    ) || 0
    cx += advance
  end
  emit_op(Pdfrb::Content::Operator::EndText)
  self
end

#translate(tx, ty) ⇒ Object



31
32
33
# File 'lib/pdfrb/content/canvas.rb', line 31

def translate(tx, ty, &)
  concat(1, 0, 0, 1, tx, ty, &)
end

#with_transparency(opacity: 1.0, blend_mode: nil) ⇒ Object



283
284
285
286
287
288
289
290
# File 'lib/pdfrb/content/canvas.rb', line 283

def with_transparency(opacity: 1.0, blend_mode: nil, &)
  save_graphics_state do
    self.opacity = opacity if opacity < 1.0
    self.blend_mode = blend_mode if blend_mode
    yield self
  end
  self
end