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.



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

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



448
449
450
451
452
453
454
# File 'lib/pdfrb/content/canvas.rb', line 448

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

#blend_mode=(mode) ⇒ Object



270
271
272
273
# File 'lib/pdfrb/content/canvas.rb', line 270

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.



111
112
113
114
# File 'lib/pdfrb/content/canvas.rb', line 111

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

#clipObject



228
229
230
231
232
# File 'lib/pdfrb/content/canvas.rb', line 228

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

#clip_even_oddObject



234
235
236
237
238
# File 'lib/pdfrb/content/canvas.rb', line 234

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

#close_pathObject



197
198
199
200
# File 'lib/pdfrb/content/canvas.rb', line 197

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
54
55
# File 'lib/pdfrb/content/canvas.rb', line 45

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

  begin
    yield self
  ensure
    emit_op Pdfrb::Content::Operator::SaveGraphicsState
  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.



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

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



187
188
189
190
# File 'lib/pdfrb/content/canvas.rb', line 187

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.



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

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



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

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.



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

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

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



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

def draw_image(name, at: nil, width: nil, height: nil, matrix: nil)
  @used_xobjects[name] = true
  save_graphics_state do
    if matrix
      a, b, c, d, e, f = matrix
      concat(a, b, c, d, e, f)
      emit_op(Pdfrb::Content::Operator::InvokeXObject, name)
    else
      translate(at[0], at[1])
      concat(width, 0, 0, height, 0, 0)
      append(" /#{name} Do\n")
    end
  end
  self
end

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



304
305
306
307
308
309
310
311
# File 'lib/pdfrb/content/canvas.rb', line 304

def draw_image_matrix(name, a:, b:, c:, d:, e:, f:)
  @used_xobjects[name] = true
  save_graphics_state do
    concat(a, b, c, d, e, f)
    emit_op(Pdfrb::Content::Operator::InvokeXObject, name)
  end
  self
end

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

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



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

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



472
473
474
475
# File 'lib/pdfrb/content/canvas.rb', line 472

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

#end_marked_contentObject



436
437
438
439
# File 'lib/pdfrb/content/canvas.rb', line 436

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

#end_pathObject



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

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

#fill(rule: :nonzero) ⇒ Object



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

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



245
246
247
248
249
250
251
252
253
# File 'lib/pdfrb/content/canvas.rb', line 245

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



240
241
242
243
# File 'lib/pdfrb/content/canvas.rb', line 240

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

#fill_stroke(rule: :nonzero) ⇒ Object



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

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).



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

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

#line(x1, y1, x2, y2) ⇒ Object



67
68
69
# File 'lib/pdfrb/content/canvas.rb', line 67

def line(x1, y1, x2, y2)
  move_to(x1, y1).line_to(x2, y2)
end

#line_cap=(n) ⇒ Object



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

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

#line_join=(n) ⇒ Object



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

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

#line_to(x, y) ⇒ Object



62
63
64
65
# File 'lib/pdfrb/content/canvas.rb', line 62

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

#line_width=(n) ⇒ Object



398
399
400
# File 'lib/pdfrb/content/canvas.rb', line 398

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

#marked_content(tag, properties = nil) ⇒ Object



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

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



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

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

#move_to(x, y) ⇒ Object



57
58
59
60
# File 'lib/pdfrb/content/canvas.rb', line 57

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

#opacity=(alpha) ⇒ Object



265
266
267
268
# File 'lib/pdfrb/content/canvas.rb', line 265

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.



84
85
86
# File 'lib/pdfrb/content/canvas.rb', line 84

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.



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

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



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

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, y, width, height) ⇒ Object



192
193
194
195
# File 'lib/pdfrb/content/canvas.rb', line 192

def rectangle(x, y, width, height)
  emit_op(Pdfrb::Content::Operator::Rectangle, x, y, width, height)
  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]).



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

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



202
203
204
205
# File 'lib/pdfrb/content/canvas.rb', line 202

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

#stroke_color(color) ⇒ Object



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

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



441
442
443
444
445
446
# File 'lib/pdfrb/content/canvas.rb', line 441

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



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

def text(str, at:, font:, size:, char_spacing: nil, word_spacing: nil)
  @used_fonts[font] = size
  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



367
368
369
370
371
372
373
374
375
376
377
# File 'lib/pdfrb/content/canvas.rb', line 367

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



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

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



275
276
277
278
279
280
281
282
283
284
285
286
# File 'lib/pdfrb/content/canvas.rb', line 275

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

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