Class: Arrolio::Renderer::Pdf

Inherits:
Object
  • Object
show all
Defined in:
lib/arrolio/renderer/pdf.rb

Overview

Walks Output::Page and emits PDF bytes via Pdfrb. Supports font embedding: pass font_paths: as a Hash mapping the Style#font_name string to a TTF path, and the renderer will subset + embed each font for the codepoints actually used in the document.

Constant Summary collapse

MM_TO_PT =
2.83464567

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializePdf

Returns a new instance of Pdf.



19
20
21
22
23
24
25
26
27
28
# File 'lib/arrolio/renderer/pdf.rb', line 19

def initialize
  @document = Pdfrb::Document.new
  @fonts = FontRegistry.new(@document)
  @images = {}
  @font_paths = {}
  @font_embedders = {}
  @font_encoders = {}
  @font_refs = {}
  @link_annotator = LinkAnnotator.new(@document)
end

Instance Attribute Details

#documentObject (readonly)

Returns the value of attribute document.



17
18
19
# File 'lib/arrolio/renderer/pdf.rb', line 17

def document
  @document
end

#fontsObject (readonly)

Returns the value of attribute fonts.



17
18
19
# File 'lib/arrolio/renderer/pdf.rb', line 17

def fonts
  @fonts
end

Instance Method Details

#apply_metadata(metadata) ⇒ Object



44
45
46
47
48
49
50
51
52
# File 'lib/arrolio/renderer/pdf.rb', line 44

def ()
  info = @document.catalog.value[:Info]
  info ||= @document.add({})
  @document.catalog.value[:Info] = info
  info.value[:Title] = [:title] if [:title]
  info.value[:Author] = [:author] if [:author]
  info.value[:Creator] = 'Arrolio (Ruby)'
  info.value[:Producer] = 'Arrolio + Pdfrb'
end

#attach_xmp_metadata(metadata) ⇒ Object

Emit an XMP metadata packet (RDF/XML) as a stream on the catalog's /Metadata entry. PDF readers use this for Dublin Core properties alongside the legacy /Info dict.



57
58
59
60
61
62
63
64
65
66
67
68
# File 'lib/arrolio/renderer/pdf.rb', line 57

def ()
  xmp = XmpBuilder.new().build
  stream = @document.add(
    { Type: :Metadata, Subtype: :XML, Length: xmp.bytesize },
    type: Pdfrb::Model::Cos::Stream
  )
  stream.stream = xmp
  @document.catalog.value[:Metadata] =
    Pdfrb::Model::Reference.new(stream.oid, stream.gen)
rescue StandardError => e
  Arrolio::Logger.warn "XMP attach failed: #{e.class}: #{e.message[0, 80]}"
end

#cover_logo_styleObject



137
138
139
140
141
142
143
144
145
146
147
148
149
# File 'lib/arrolio/renderer/pdf.rb', line 137

def cover_logo_style
  return @cover_logo_style if @cover_logo_style

  config = @layout_spec&.cover_logo_config || {}
  width_mm = config['width_mm'] || 35.0
  ratio = config['aspect_ratio'] || (1459.0 / 1667.0)
  margin_mm = config['margin_mm'] || 25.5
  @cover_logo_style = {
    width: width_mm * MM_TO_PT,
    height: width_mm * ratio * MM_TO_PT,
    margin: margin_mm * MM_TO_PT
  }.freeze
end


121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/arrolio/renderer/pdf.rb', line 121

def header_footer_style
  return @header_footer_style if @header_footer_style

  config = @layout_spec&.header_footer_config || {}
  @header_footer_style = {
    margin_top: (config['margin_top'] || 26.5) * MM_TO_PT,
    margin_bottom: (config['margin_bottom'] || 26.5) * MM_TO_PT,
    margin_lr: (config['margin_lr'] || 25.5) * MM_TO_PT,
    header_offset: (config['header_offset'] || 4) * MM_TO_PT,
    footer_offset: (config['footer_offset'] || 4) * MM_TO_PT,
    font_name: config['font_name'] || 'Helvetica',
    font_size: config['font_size'] || 9.0,
    rule_width: config['rule_width'] || 0.5
  }.freeze
end

#rasterize_svg(svg_path, png_path) ⇒ Object



110
111
112
113
114
115
116
117
118
119
# File 'lib/arrolio/renderer/pdf.rb', line 110

def rasterize_svg(svg_path, png_path)
  require 'open3'
  result = Open3.capture3('rsvg-convert', '-d', '96', '-p', '96',
                          '-o', png_path, svg_path)
  return if result[2].success?

  Arrolio::Logger.warn "rsvg-convert failed: #{result[1]}"
rescue StandardError => e
  Arrolio::Logger.warn "rasterize_svg failed: #{e.class}: #{e.message[0, 80]}"
end

#register_image(path) ⇒ Object



79
80
81
82
83
84
85
86
87
88
89
# File 'lib/arrolio/renderer/pdf.rb', line 79

def register_image(path)
  return @images[path] if @images.key?(path)

  resolved = resolve_image_for_pdfrb(path)
  return nil unless resolved

  @images[path] = @document.images.add(resolved)
rescue StandardError => e
  Arrolio::Logger.warn "register_image failed for #{path}: #{e.class}: #{e.message[0, 80]}"
  nil
end

#register_logo(path) ⇒ Object



70
71
72
73
74
75
76
77
# File 'lib/arrolio/renderer/pdf.rb', line 70

def (path)
  return nil unless path

  @document.images.add(path)
rescue StandardError => e
  Arrolio::Logger.warn "logo load failed: #{e.class}: #{e.message[0, 80]}"
  nil
end

#render(pages, io:, logo_path: nil, metadata: {}, font_paths: {}, context: nil, layout_spec: nil) ⇒ Object



30
31
32
33
34
35
36
37
38
39
40
41
42
# File 'lib/arrolio/renderer/pdf.rb', line 30

def render(pages, io:, logo_path: nil, metadata: {},
           font_paths: {}, context: nil, layout_spec: nil)
  @font_paths = font_paths.transform_keys(&:to_s)
  @layout_spec = layout_spec
  ()
  ()
  @logo_ref = (logo_path)
  prepare_embedded_fonts(pages)
  pages.each { |page| render_page(page) }
  build_outline(context, pages) if context
  @document.write(io.is_a?(String) ? io : nil,
                  io: io.is_a?(String) ? nil : io)
end

#resolve_image_for_pdfrb(path) ⇒ Object

Pdfrb supports JPEG, PNG, and PDF — not SVG. If path is an SVG file, rasterize it to PNG via rsvg-convert and cache the result. Returns the path to a pdfrb-compatible image file.



94
95
96
97
98
99
100
101
102
103
# File 'lib/arrolio/renderer/pdf.rb', line 94

def resolve_image_for_pdfrb(path)
  return path unless path.to_s.end_with?('.svg', '.SVG')
  return path unless File.exist?(path)

  png_path = svg_to_png_cache_path(path)
  return png_path if File.exist?(png_path)

  rasterize_svg(path, png_path)
  File.exist?(png_path) ? png_path : path
end

#svg_to_png_cache_path(svg_path) ⇒ Object



105
106
107
108
# File 'lib/arrolio/renderer/pdf.rb', line 105

def svg_to_png_cache_path(svg_path)
  digest = Digest::MD5.file(svg_path).hexdigest
  File.join(Dir.tmpdir, 'arrolio-svg-' + digest + '.png')
end