Class: Idml::Render::PdfWriter

Inherits:
Object
  • Object
show all
Defined in:
lib/idml/render/pdf_writer.rb

Overview

Minimal PDF file writer. Assembles content-stream strings into a valid PDF with page tree, font resources, image XObjects, embedded TrueType fonts, cross-reference table, and trailer.

Instance Method Summary collapse

Constructor Details

#initializePdfWriter

Returns a new instance of PdfWriter.



14
15
16
17
18
19
20
21
22
23
# File 'lib/idml/render/pdf_writer.rb', line 14

def initialize
  @pages = []
  @images = {}
  @embedded_fonts = {}
  @info = {}
  @next_id = 1
  @catalog_id = alloc_id
  @pages_id = alloc_id
  @info_id = alloc_id
end

Instance Method Details

#add_bookmark(title, page_index) ⇒ Object

Add a bookmark (outline entry) pointing to a page. title: display text; page_index: 0-based page index.



144
145
146
147
# File 'lib/idml/render/pdf_writer.rb', line 144

def add_bookmark(title, page_index)
  @outlines ||= []
  @outlines << { title: title, page_index: page_index }
end

#add_image(data:) ⇒ Object

Auto-detect format and register the image.



64
65
66
67
68
69
70
71
72
73
# File 'lib/idml/render/pdf_writer.rb', line 64

def add_image(data:)
  format = Render::Image.detect_format(data)
  return nil unless format

  dims = image_dimensions(data, format)
  return nil unless dims

  cs = image_colorspace(data, format) || :DeviceRGB
  register_format_image(data, format, dims, cs)
end

#add_image_object(data:, width:, height:, colorspace:, filter:) ⇒ Object



102
103
104
105
106
107
108
109
110
111
112
113
114
# File 'lib/idml/render/pdf_writer.rb', line 102

def add_image_object(data:, width:, height:, colorspace:, filter:)
  name = "Im#{@images.length + 1}"
  id = alloc_id
  @images[name] = {
    id: id,
    data: data.dup.force_encoding("ASCII-8BIT"),
    width: width,
    height: height,
    colorspace: colorspace,
    filter: filter,
  }
  name
end

#add_jpeg_image(data:, width:, height:, colorspace: :DeviceRGB) ⇒ Object

Register a JPEG image as a PDF image XObject (DCTDecode). Returns the XObject name to use in content streams (e.g. "Im1").



45
46
47
48
# File 'lib/idml/render/pdf_writer.rb', line 45

def add_jpeg_image(data:, width:, height:, colorspace: :DeviceRGB)
  add_image_object(data: data, width: width, height: height,
                   colorspace: colorspace, filter: :DCTDecode)
end

#add_page(width:, height:, content:, fonts: {}, xobjects: []) ⇒ Object



25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
# File 'lib/idml/render/pdf_writer.rb', line 25

def add_page(width:, height:, content:, fonts: {}, xobjects: [])
  font_objs = build_fonts(fonts)
  xobject_objs = build_xobjects(xobjects)
  content_id = alloc_id
  page_id = alloc_id

  @pages << {
    id: page_id,
    width: width,
    height: height,
    content_id: content_id,
    content: content,
    font_objs: font_objs,
    xobject_objs: xobject_objs,
  }
  page_id
end

#add_png_image(data:, width:, height:, colorspace: :DeviceRGB) ⇒ Object

Register a PNG image as a PDF image XObject (FlateDecode with PNG predictor). The IDAT chunk data is embedded directly; the PDF viewer handles decompression and unfiltering.



53
54
55
56
57
58
59
60
61
# File 'lib/idml/render/pdf_writer.rb', line 53

def add_png_image(data:, width:, height:, colorspace: :DeviceRGB)
  idat = Render::Image.png_idat_data(data)
  return nil unless idat

  name = add_image_object(data: idat, width: width, height: height,
                          colorspace: colorspace, filter: :FlateDecode)
  @images[name][:decode_parms] = png_decode_parms(width, colorspace)
  name
end

#image_colorspace(data, format) ⇒ Object



83
84
85
86
87
88
89
# File 'lib/idml/render/pdf_writer.rb', line 83

def image_colorspace(data, format)
  if format == :png
    Render::Image.png_colorspace(data)
  else
    Render::Image.jpeg_colorspace(data)
  end
end

#image_dimensions(data, format) ⇒ Object



75
76
77
78
79
80
81
# File 'lib/idml/render/pdf_writer.rb', line 75

def image_dimensions(data, format)
  if format == :png
    Render::Image.png_dimensions(data)
  else
    Render::Image.jpeg_dimensions(data)
  end
end

#png_decode_parms(width, colorspace) ⇒ Object



116
117
118
119
# File 'lib/idml/render/pdf_writer.rb', line 116

def png_decode_parms(width, colorspace)
  colors = colorspace == :DeviceGray ? 1 : 3
  "<< /Predictor 15 /Columns #{width} /Colors #{colors} /BitsPerComponent 8 >>"
end

#register_embedded_font(metrics:, data:) ⇒ Object

Register a TrueType font for embedding (FontFile2). Returns the PostScript name to use as the value in add_page's fonts hash.



123
124
125
126
127
128
129
130
# File 'lib/idml/render/pdf_writer.rb', line 123

def register_embedded_font(metrics:, data:)
  ps_name = metrics.postscript_name
  @embedded_fonts[ps_name] = {
    metrics: metrics,
    data: data.dup.force_encoding("ASCII-8BIT"),
  }
  ps_name
end

#register_format_image(data, format, dims, cs) ⇒ Object



91
92
93
94
95
96
97
98
99
100
# File 'lib/idml/render/pdf_writer.rb', line 91

def register_format_image(data, format, dims, cs)
  case format
  when :jpeg
    add_jpeg_image(data: data, width: dims[0], height: dims[1],
                   colorspace: cs)
  when :png
    add_png_image(data: data, width: dims[0], height: dims[1],
                  colorspace: cs)
  end
end

#set_info(hash) ⇒ Object

Set PDF metadata (Title, Author, Subject, etc.). Values should be strings; keys are PDF Info dictionary keys.



138
139
140
# File 'lib/idml/render/pdf_writer.rb', line 138

def set_info(hash)
  @info.merge!(hash)
end

#set_output_intent(profile_data) ⇒ Object

Set OutputIntent with an ICC profile. Required for PDF/A. profile_data: binary ICC profile data.



157
158
159
# File 'lib/idml/render/pdf_writer.rb', line 157

def set_output_intent(profile_data)
  @icc_profile = profile_data
end

#set_xmp(xml) ⇒ Object

Set XMP metadata packet (XML string). Embedded as a stream object referenced from the Catalog.



151
152
153
# File 'lib/idml/render/pdf_writer.rb', line 151

def set_xmp(xml)
  @xmp = xml
end

#write(path) ⇒ Object



132
133
134
# File 'lib/idml/render/pdf_writer.rb', line 132

def write(path)
  File.binwrite(path, build_pdf)
end