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.



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

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



459
460
461
462
463
464
465
# File 'lib/pdfrb/content/canvas.rb', line 459

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

#blend_mode=(mode) ⇒ Object



280
281
282
283
# File 'lib/pdfrb/content/canvas.rb', line 280

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.



115
116
117
118
# File 'lib/pdfrb/content/canvas.rb', line 115

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

#clipObject



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

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

#clip_even_oddObject



244
245
246
247
248
# File 'lib/pdfrb/content/canvas.rb', line 244

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

#close_pathObject



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

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.



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

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



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

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.



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

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



425
426
427
428
# File 'lib/pdfrb/content/canvas.rb', line 425

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.



329
330
331
332
333
334
335
336
337
338
339
340
341
# File 'lib/pdfrb/content/canvas.rb', line 329

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



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
# File 'lib/pdfrb/content/canvas.rb', line 298

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



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

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.



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

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



483
484
485
486
# File 'lib/pdfrb/content/canvas.rb', line 483

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

#end_marked_contentObject



447
448
449
450
# File 'lib/pdfrb/content/canvas.rb', line 447

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

#end_pathObject



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

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

#fill(rule: :nonzero) ⇒ Object



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

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



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

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



250
251
252
253
# File 'lib/pdfrb/content/canvas.rb', line 250

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

#fill_stroke(rule: :nonzero) ⇒ Object



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

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



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

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 = nil, y1 = nil, x2 = nil, y2 = nil, from: nil, to: nil) ⇒ Object



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

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



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

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

#line_join=(n) ⇒ Object



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

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



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

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

#marked_content(tag, properties = nil) ⇒ Object



430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
# File 'lib/pdfrb/content/canvas.rb', line 430

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



421
422
423
# File 'lib/pdfrb/content/canvas.rb', line 421

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



275
276
277
278
# File 'lib/pdfrb/content/canvas.rb', line 275

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.



88
89
90
# File 'lib/pdfrb/content/canvas.rb', line 88

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.



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

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



467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
# File 'lib/pdfrb/content/canvas.rb', line 467

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)


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

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



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

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



212
213
214
215
# File 'lib/pdfrb/content/canvas.rb', line 212

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

#stroke_color(color) ⇒ Object



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

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



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

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



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

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



378
379
380
381
382
383
384
385
386
387
388
# File 'lib/pdfrb/content/canvas.rb', line 378

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



390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
# File 'lib/pdfrb/content/canvas.rb', line 390

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



285
286
287
288
289
290
291
292
293
294
295
296
# File 'lib/pdfrb/content/canvas.rb', line 285

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