idml 0.1.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +12 -7
- data/TODO.pdf/51-remove-dead-code.md +9 -2
- data/TODO.pdf/53-tagged-pdf-accessibility.md +9 -1
- data/TODO.pdf/61-known-limitations.md +35 -0
- data/TODO.pdf/62-pdfrb-015-migration.md +44 -0
- data/TODO.pdf/63-replace-fontmetrics-with-pdfrb.md +31 -0
- data/idml.gemspec +1 -1
- data/lib/idml/cli.rb +12 -4
- data/lib/idml/render/gradient_resolver.rb +9 -8
- data/lib/idml/render/image.rb +5 -10
- data/lib/idml/render/pdfrb_writer.rb +46 -55
- data/lib/idml/render/pipeline.rb +4 -1
- data/lib/idml/render/spread_renderer.rb +7 -5
- data/lib/idml/render.rb +17 -24
- data/lib/idml/version.rb +1 -1
- metadata +6 -14
- data/TODO.pdf/56-text-engine-pdfrb-integration.md +0 -33
- data/TODO.pdf/57-image-clipping-pdfrb.md +0 -26
- data/TODO.pdf/58-bookmarks-pdfrb.md +0 -23
- data/lib/idml/render/color.rb +0 -69
- data/lib/idml/render/font_embedder.rb +0 -79
- data/lib/idml/render/path.rb +0 -80
- data/lib/idml/render/pdf_writer.rb +0 -426
- data/lib/idml/render/pdfrb_ext/clip.rb +0 -31
- data/lib/idml/render/pdfrb_ext/invoke_xobject.rb +0 -24
- data/lib/idml/render/pdfrb_ext.rb +0 -13
- data/lib/idml/render/text.rb +0 -91
|
@@ -1,426 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# DEPRECATED: This module is superseded by Idml::Render::PdfrbWriter.
|
|
4
|
-
# The Pipeline now uses pdfrb (Pdfrb::Document) for all PDF assembly.
|
|
5
|
-
# This module remains for backward compatibility but is not used by
|
|
6
|
-
# any active code path. See TODO.pdf/51-remove-dead-code.md.
|
|
7
|
-
|
|
8
|
-
module Idml
|
|
9
|
-
module Render
|
|
10
|
-
# Minimal PDF file writer. Assembles content-stream strings into
|
|
11
|
-
# a valid PDF with page tree, font resources, image XObjects,
|
|
12
|
-
# embedded TrueType fonts, cross-reference table, and trailer.
|
|
13
|
-
class PdfWriter
|
|
14
|
-
def initialize
|
|
15
|
-
@pages = []
|
|
16
|
-
@images = {}
|
|
17
|
-
@embedded_fonts = {}
|
|
18
|
-
@info = {}
|
|
19
|
-
@next_id = 1
|
|
20
|
-
@catalog_id = alloc_id
|
|
21
|
-
@pages_id = alloc_id
|
|
22
|
-
@info_id = alloc_id
|
|
23
|
-
end
|
|
24
|
-
|
|
25
|
-
def add_page(width:, height:, content:, fonts: {}, xobjects: [])
|
|
26
|
-
font_objs = build_fonts(fonts)
|
|
27
|
-
xobject_objs = build_xobjects(xobjects)
|
|
28
|
-
content_id = alloc_id
|
|
29
|
-
page_id = alloc_id
|
|
30
|
-
|
|
31
|
-
@pages << {
|
|
32
|
-
id: page_id,
|
|
33
|
-
width: width,
|
|
34
|
-
height: height,
|
|
35
|
-
content_id: content_id,
|
|
36
|
-
content: content,
|
|
37
|
-
font_objs: font_objs,
|
|
38
|
-
xobject_objs: xobject_objs,
|
|
39
|
-
}
|
|
40
|
-
page_id
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Register a JPEG image as a PDF image XObject (DCTDecode).
|
|
44
|
-
# Returns the XObject name to use in content streams (e.g. "Im1").
|
|
45
|
-
def add_jpeg_image(data:, width:, height:, colorspace: :DeviceRGB)
|
|
46
|
-
add_image_object(data: data, width: width, height: height,
|
|
47
|
-
colorspace: colorspace, filter: :DCTDecode)
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# Register a PNG image as a PDF image XObject (FlateDecode with
|
|
51
|
-
# PNG predictor). The IDAT chunk data is embedded directly; the
|
|
52
|
-
# PDF viewer handles decompression and unfiltering.
|
|
53
|
-
def add_png_image(data:, width:, height:, colorspace: :DeviceRGB)
|
|
54
|
-
idat = Render::Image.png_idat_data(data)
|
|
55
|
-
return nil unless idat
|
|
56
|
-
|
|
57
|
-
name = add_image_object(data: idat, width: width, height: height,
|
|
58
|
-
colorspace: colorspace, filter: :FlateDecode)
|
|
59
|
-
@images[name][:decode_parms] = png_decode_parms(width, colorspace)
|
|
60
|
-
name
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# Auto-detect format and register the image.
|
|
64
|
-
def add_image(data:)
|
|
65
|
-
format = Render::Image.detect_format(data)
|
|
66
|
-
return nil unless format
|
|
67
|
-
|
|
68
|
-
dims = image_dimensions(data, format)
|
|
69
|
-
return nil unless dims
|
|
70
|
-
|
|
71
|
-
cs = image_colorspace(data, format) || :DeviceRGB
|
|
72
|
-
register_format_image(data, format, dims, cs)
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def image_dimensions(data, format)
|
|
76
|
-
if format == :png
|
|
77
|
-
Render::Image.png_dimensions(data)
|
|
78
|
-
else
|
|
79
|
-
Render::Image.jpeg_dimensions(data)
|
|
80
|
-
end
|
|
81
|
-
end
|
|
82
|
-
|
|
83
|
-
def image_colorspace(data, format)
|
|
84
|
-
if format == :png
|
|
85
|
-
Render::Image.png_colorspace(data)
|
|
86
|
-
else
|
|
87
|
-
Render::Image.jpeg_colorspace(data)
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def register_format_image(data, format, dims, cs)
|
|
92
|
-
case format
|
|
93
|
-
when :jpeg
|
|
94
|
-
add_jpeg_image(data: data, width: dims[0], height: dims[1],
|
|
95
|
-
colorspace: cs)
|
|
96
|
-
when :png
|
|
97
|
-
add_png_image(data: data, width: dims[0], height: dims[1],
|
|
98
|
-
colorspace: cs)
|
|
99
|
-
end
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
def add_image_object(data:, width:, height:, colorspace:, filter:)
|
|
103
|
-
name = "Im#{@images.length + 1}"
|
|
104
|
-
id = alloc_id
|
|
105
|
-
@images[name] = {
|
|
106
|
-
id: id,
|
|
107
|
-
data: data.dup.force_encoding("ASCII-8BIT"),
|
|
108
|
-
width: width,
|
|
109
|
-
height: height,
|
|
110
|
-
colorspace: colorspace,
|
|
111
|
-
filter: filter,
|
|
112
|
-
}
|
|
113
|
-
name
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
def png_decode_parms(width, colorspace)
|
|
117
|
-
colors = colorspace == :DeviceGray ? 1 : 3
|
|
118
|
-
"<< /Predictor 15 /Columns #{width} /Colors #{colors} /BitsPerComponent 8 >>"
|
|
119
|
-
end
|
|
120
|
-
|
|
121
|
-
# Register a TrueType font for embedding (FontFile2). Returns the
|
|
122
|
-
# PostScript name to use as the value in add_page's fonts hash.
|
|
123
|
-
def register_embedded_font(metrics:, data:)
|
|
124
|
-
ps_name = metrics.postscript_name
|
|
125
|
-
@embedded_fonts[ps_name] = {
|
|
126
|
-
metrics: metrics,
|
|
127
|
-
data: data.dup.force_encoding("ASCII-8BIT"),
|
|
128
|
-
}
|
|
129
|
-
ps_name
|
|
130
|
-
end
|
|
131
|
-
|
|
132
|
-
def write(path)
|
|
133
|
-
File.binwrite(path, build_pdf)
|
|
134
|
-
end
|
|
135
|
-
|
|
136
|
-
# Set PDF metadata (Title, Author, Subject, etc.).
|
|
137
|
-
# Values should be strings; keys are PDF Info dictionary keys.
|
|
138
|
-
def set_info(hash)
|
|
139
|
-
@info.merge!(hash)
|
|
140
|
-
end
|
|
141
|
-
|
|
142
|
-
# Add a bookmark (outline entry) pointing to a page.
|
|
143
|
-
# title: display text; page_index: 0-based page index.
|
|
144
|
-
def add_bookmark(title, page_index)
|
|
145
|
-
@outlines ||= []
|
|
146
|
-
@outlines << { title: title, page_index: page_index }
|
|
147
|
-
end
|
|
148
|
-
|
|
149
|
-
# Set XMP metadata packet (XML string). Embedded as a stream
|
|
150
|
-
# object referenced from the Catalog.
|
|
151
|
-
def set_xmp(xml)
|
|
152
|
-
@xmp = xml
|
|
153
|
-
end
|
|
154
|
-
|
|
155
|
-
# Set OutputIntent with an ICC profile. Required for PDF/A.
|
|
156
|
-
# profile_data: binary ICC profile data.
|
|
157
|
-
def set_output_intent(profile_data)
|
|
158
|
-
@icc_profile = profile_data
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
private
|
|
162
|
-
|
|
163
|
-
def alloc_id
|
|
164
|
-
id = @next_id
|
|
165
|
-
@next_id += 1
|
|
166
|
-
id
|
|
167
|
-
end
|
|
168
|
-
|
|
169
|
-
def build_fonts(fonts)
|
|
170
|
-
fonts.map do |name, ps_name|
|
|
171
|
-
embedded = @embedded_fonts[ps_name]
|
|
172
|
-
entry = {
|
|
173
|
-
name: name,
|
|
174
|
-
ps_name: ps_name,
|
|
175
|
-
id: alloc_id,
|
|
176
|
-
embedded: embedded,
|
|
177
|
-
}
|
|
178
|
-
if embedded
|
|
179
|
-
entry[:descriptor_id] = alloc_id
|
|
180
|
-
entry[:file_id] = alloc_id
|
|
181
|
-
end
|
|
182
|
-
entry
|
|
183
|
-
end
|
|
184
|
-
end
|
|
185
|
-
|
|
186
|
-
# xobjects: Array of names referencing registered images.
|
|
187
|
-
def build_xobjects(xobject_names)
|
|
188
|
-
xobject_names.filter_map do |name|
|
|
189
|
-
img = @images[name]
|
|
190
|
-
next unless img
|
|
191
|
-
|
|
192
|
-
{ name: name, id: img[:id] }
|
|
193
|
-
end
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
def build_pdf
|
|
197
|
-
objects = {}
|
|
198
|
-
|
|
199
|
-
objects[@catalog_id] = build_catalog(objects)
|
|
200
|
-
|
|
201
|
-
page_refs = @pages.map { |p| "#{p[:id]} 0 R" }.join(" ")
|
|
202
|
-
objects[@pages_id] =
|
|
203
|
-
"<< /Type /Pages /Kids [#{page_refs}] /Count #{@pages.length} >>"
|
|
204
|
-
|
|
205
|
-
@pages.each { |page| build_page_objects(page, objects) }
|
|
206
|
-
|
|
207
|
-
@images.each_value do |img|
|
|
208
|
-
objects[img[:id]] = build_image_xobject(img)
|
|
209
|
-
end
|
|
210
|
-
|
|
211
|
-
objects[@info_id] = build_info_object
|
|
212
|
-
|
|
213
|
-
assemble_pdf(objects)
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
def build_catalog(objects)
|
|
217
|
-
dict = "<< /Type /Catalog /Pages #{@pages_id} 0 R"
|
|
218
|
-
outlines_ref = build_outlines(objects)
|
|
219
|
-
dict << " /Outlines #{outlines_ref}" if outlines_ref
|
|
220
|
-
xmp_ref = build_xmp(objects)
|
|
221
|
-
dict << " /Metadata #{xmp_ref}" if xmp_ref
|
|
222
|
-
output_ref = build_output_intent(objects)
|
|
223
|
-
dict << " /OutputIntents [#{output_ref}]" if output_ref
|
|
224
|
-
"#{dict} >>"
|
|
225
|
-
end
|
|
226
|
-
|
|
227
|
-
def build_outlines(objects)
|
|
228
|
-
return nil unless @outlines&.any?
|
|
229
|
-
|
|
230
|
-
outlines_id = alloc_id
|
|
231
|
-
item_ids = @outlines.map { alloc_id }
|
|
232
|
-
@outlines.each_with_index do |entry, i|
|
|
233
|
-
objects[item_ids[i]] = outline_item(entry, i, item_ids, outlines_id)
|
|
234
|
-
end
|
|
235
|
-
objects[outlines_id] = outlines_root(outlines_id, item_ids)
|
|
236
|
-
"#{outlines_id} 0 R"
|
|
237
|
-
end
|
|
238
|
-
|
|
239
|
-
def outline_item(entry, index, item_ids, outlines_id)
|
|
240
|
-
prev_ref = index.positive? ? " /Prev #{item_ids[index - 1]} 0 R" : ""
|
|
241
|
-
next_ref = next_outline_ref(index, item_ids)
|
|
242
|
-
page_ref = outline_page_ref(entry[:page_index])
|
|
243
|
-
title = escape_pdf_string(entry[:title].to_s)
|
|
244
|
-
"<< /Title (#{title}) /Parent #{outlines_id} 0 R" \
|
|
245
|
-
"#{prev_ref}#{next_ref} /Dest [#{page_ref} 0 R /Fit] >>"
|
|
246
|
-
end
|
|
247
|
-
|
|
248
|
-
def next_outline_ref(index, item_ids)
|
|
249
|
-
return "" unless index < item_ids.length - 1
|
|
250
|
-
|
|
251
|
-
" /Next #{item_ids[index + 1]} 0 R"
|
|
252
|
-
end
|
|
253
|
-
|
|
254
|
-
def outline_page_ref(page_index)
|
|
255
|
-
page = @pages[page_index] || @pages.first
|
|
256
|
-
page ? page[:id] : @pages_id
|
|
257
|
-
end
|
|
258
|
-
|
|
259
|
-
def outlines_root(_outlines_id, item_ids)
|
|
260
|
-
"<< /Type /Outlines /First #{item_ids.first} 0 R " \
|
|
261
|
-
"/Last #{item_ids.last} 0 R /Count #{item_ids.length} >>"
|
|
262
|
-
end
|
|
263
|
-
|
|
264
|
-
def build_info_object
|
|
265
|
-
entries = @info.map do |key, value|
|
|
266
|
-
"/#{key} (#{escape_pdf_string(value.to_s)})"
|
|
267
|
-
end.join(" ")
|
|
268
|
-
"<< #{entries} >>"
|
|
269
|
-
end
|
|
270
|
-
|
|
271
|
-
def escape_pdf_string(str)
|
|
272
|
-
str.gsub("\\", "\\\\\\\\").gsub("(", "\\(").gsub(")", "\\)")
|
|
273
|
-
end
|
|
274
|
-
|
|
275
|
-
def build_xmp(objects)
|
|
276
|
-
return nil unless @xmp
|
|
277
|
-
|
|
278
|
-
xmp_id = alloc_id
|
|
279
|
-
data = @xmp.dup.force_encoding("ASCII-8BIT")
|
|
280
|
-
objects[xmp_id] = "<< /Type /Metadata /Subtype /XML " \
|
|
281
|
-
"/Length #{data.bytesize} >>\nstream\n#{data}\nendstream"
|
|
282
|
-
"#{xmp_id} 0 R"
|
|
283
|
-
end
|
|
284
|
-
|
|
285
|
-
def build_output_intent(objects)
|
|
286
|
-
return nil unless @icc_profile
|
|
287
|
-
|
|
288
|
-
icc_id = alloc_id
|
|
289
|
-
data = @icc_profile.dup.force_encoding("ASCII-8BIT")
|
|
290
|
-
objects[icc_id] = "<< /N 3 /Alternate /DeviceRGB " \
|
|
291
|
-
"/Length #{data.bytesize} >>\nstream\n#{data}\nendstream"
|
|
292
|
-
intent_id = alloc_id
|
|
293
|
-
objects[intent_id] = "<< /Type /OutputIntent /S /GTS_PDFA1 " \
|
|
294
|
-
"/OutputConditionIdentifier (sRGB) " \
|
|
295
|
-
"/Info (sRGB IEC61966-2.1) " \
|
|
296
|
-
"/DestOutputProfile #{icc_id} 0 R >>"
|
|
297
|
-
"#{intent_id} 0 R"
|
|
298
|
-
end
|
|
299
|
-
|
|
300
|
-
def build_page_objects(page, objects)
|
|
301
|
-
font_dict = page[:font_objs].map do |f|
|
|
302
|
-
"/#{f[:name]} #{f[:id]} 0 R"
|
|
303
|
-
end.join(" ")
|
|
304
|
-
xobject_dict = page[:xobject_objs].map do |x|
|
|
305
|
-
"/#{x[:name]} #{x[:id]} 0 R"
|
|
306
|
-
end.join(" ")
|
|
307
|
-
objects[page[:id]] = build_page_object(page, font_dict, xobject_dict)
|
|
308
|
-
objects[page[:content_id]] = build_content_stream(page[:content])
|
|
309
|
-
page[:font_objs].each do |f|
|
|
310
|
-
if f[:embedded]
|
|
311
|
-
build_embedded_font_objects(f, objects)
|
|
312
|
-
else
|
|
313
|
-
objects[f[:id]] = build_type1_font(f[:ps_name])
|
|
314
|
-
end
|
|
315
|
-
end
|
|
316
|
-
end
|
|
317
|
-
|
|
318
|
-
def build_embedded_font_objects(f, objects)
|
|
319
|
-
metrics = f[:embedded][:metrics]
|
|
320
|
-
data = f[:embedded][:data]
|
|
321
|
-
desc = Render::FontEmbedder.descriptor(metrics)
|
|
322
|
-
widths = Render::FontEmbedder.widths_array(metrics)
|
|
323
|
-
|
|
324
|
-
objects[f[:id]] = build_truetype_font(f, desc, widths)
|
|
325
|
-
objects[f[:descriptor_id]] = build_font_descriptor(desc, f[:file_id])
|
|
326
|
-
objects[f[:file_id]] = build_fontfile2(data)
|
|
327
|
-
end
|
|
328
|
-
|
|
329
|
-
def build_page_object(page, font_dict, xobject_dict)
|
|
330
|
-
resources = "<< /Font << #{font_dict} >>"
|
|
331
|
-
resources << " /XObject << #{xobject_dict} >>" unless xobject_dict.empty?
|
|
332
|
-
resources << " >>"
|
|
333
|
-
"<< /Type /Page /Parent #{@pages_id} 0 R " \
|
|
334
|
-
"/MediaBox [0 0 #{page[:width]} #{page[:height]}] " \
|
|
335
|
-
"/Contents #{page[:content_id]} 0 R " \
|
|
336
|
-
"/Resources #{resources} >>"
|
|
337
|
-
end
|
|
338
|
-
|
|
339
|
-
def build_content_stream(content)
|
|
340
|
-
"<< /Length #{content.bytesize} >>\nstream\n#{content}\nendstream"
|
|
341
|
-
end
|
|
342
|
-
|
|
343
|
-
def build_type1_font(ps_name)
|
|
344
|
-
"<< /Type /Font /Subtype /Type1 /BaseFont /#{ps_name} >>"
|
|
345
|
-
end
|
|
346
|
-
|
|
347
|
-
def build_truetype_font(f, desc, widths)
|
|
348
|
-
"<< /Type /Font /Subtype /TrueType " \
|
|
349
|
-
"/BaseFont /#{desc[:font_name]} " \
|
|
350
|
-
"/FirstChar #{Render::FontEmbedder::FIRST_CHAR} " \
|
|
351
|
-
"/LastChar #{Render::FontEmbedder::LAST_CHAR} " \
|
|
352
|
-
"/Widths [#{widths.join(' ')}] " \
|
|
353
|
-
"/FontDescriptor #{f[:descriptor_id]} 0 R " \
|
|
354
|
-
"/Encoding /WinAnsiEncoding >>"
|
|
355
|
-
end
|
|
356
|
-
|
|
357
|
-
def build_font_descriptor(desc, file_id)
|
|
358
|
-
bbox = desc[:font_bbox].join(" ")
|
|
359
|
-
"<< /Type /FontDescriptor /FontName /#{desc[:font_name]} " \
|
|
360
|
-
"/Flags #{desc[:flags]} " \
|
|
361
|
-
"/FontBBox [#{bbox}] " \
|
|
362
|
-
"/ItalicAngle #{desc[:italic_angle]} " \
|
|
363
|
-
"/Ascent #{desc[:ascent]} /Descent #{desc[:descent]} " \
|
|
364
|
-
"/CapHeight #{desc[:cap_height]} /StemV #{desc[:stem_v]} " \
|
|
365
|
-
"/FontFile2 #{file_id} 0 R >>"
|
|
366
|
-
end
|
|
367
|
-
|
|
368
|
-
def build_fontfile2(data)
|
|
369
|
-
result = String.new(encoding: "ASCII-8BIT")
|
|
370
|
-
result << "<< /Length #{data.bytesize} /Length1 #{data.bytesize} >>\n"
|
|
371
|
-
result << "stream\n"
|
|
372
|
-
result << data
|
|
373
|
-
result << "\nendstream"
|
|
374
|
-
result
|
|
375
|
-
end
|
|
376
|
-
|
|
377
|
-
def build_image_xobject(img)
|
|
378
|
-
data = img[:data]
|
|
379
|
-
filter = img[:filter] || :DCTDecode
|
|
380
|
-
result = String.new(encoding: "ASCII-8BIT")
|
|
381
|
-
result << "<< /Type /XObject /Subtype /Image /Width #{img[:width]} "
|
|
382
|
-
result << "/Height #{img[:height]} /BitsPerComponent 8 "
|
|
383
|
-
result << "/ColorSpace /#{img[:colorspace]} /Filter /#{filter} "
|
|
384
|
-
if img[:decode_parms]
|
|
385
|
-
result << "/DecodeParms #{img[:decode_parms]} "
|
|
386
|
-
end
|
|
387
|
-
result << "/Length #{data.bytesize} >>\nstream\n"
|
|
388
|
-
result << data
|
|
389
|
-
result << "\nendstream"
|
|
390
|
-
result
|
|
391
|
-
end
|
|
392
|
-
|
|
393
|
-
def assemble_pdf(objects)
|
|
394
|
-
pdf = "%PDF-1.4\n%\xe2\xe3\xcf\xd3\n".b
|
|
395
|
-
offsets = {}
|
|
396
|
-
(1...@next_id).each do |id|
|
|
397
|
-
next unless objects[id]
|
|
398
|
-
|
|
399
|
-
offsets[id] = pdf.bytesize
|
|
400
|
-
pdf << "#{id} 0 obj\n"
|
|
401
|
-
pdf << to_binary(objects[id])
|
|
402
|
-
pdf << "\nendobj\n"
|
|
403
|
-
end
|
|
404
|
-
xref_pos = pdf.bytesize
|
|
405
|
-
pdf << "xref\n0 #{@next_id}\n0000000000 65535 f \n"
|
|
406
|
-
(1...@next_id).each do |id|
|
|
407
|
-
pdf << if offsets[id]
|
|
408
|
-
format("%010d 00000 n \n", offsets[id])
|
|
409
|
-
else
|
|
410
|
-
"0000000000 65535 f \n"
|
|
411
|
-
end
|
|
412
|
-
end
|
|
413
|
-
pdf << "trailer\n<< /Size #{@next_id} /Root #{@catalog_id} 0 R " \
|
|
414
|
-
"/Info #{@info_id} 0 R >>\n"
|
|
415
|
-
pdf << "startxref\n#{xref_pos}\n%%EOF"
|
|
416
|
-
pdf
|
|
417
|
-
end
|
|
418
|
-
|
|
419
|
-
def to_binary(str)
|
|
420
|
-
return str if str.encoding == Encoding::ASCII_8BIT
|
|
421
|
-
|
|
422
|
-
str.dup.force_encoding("ASCII-8BIT")
|
|
423
|
-
end
|
|
424
|
-
end
|
|
425
|
-
end
|
|
426
|
-
end
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Idml
|
|
4
|
-
module Render
|
|
5
|
-
module PdfrbExt
|
|
6
|
-
# PDF `W` operator: intersect current path with clipping path
|
|
7
|
-
# (nonzero winding rule). Must be followed by `n` to end the path.
|
|
8
|
-
class Clip < Pdfrb::Content::Operator::NoArg
|
|
9
|
-
class << self
|
|
10
|
-
def name
|
|
11
|
-
"W"
|
|
12
|
-
end
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
register
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# PDF `n` operator: end path without painting. Commonly used
|
|
19
|
-
# after `W` to set a clip without filling or stroking.
|
|
20
|
-
class EndPath < Pdfrb::Content::Operator::NoArg
|
|
21
|
-
class << self
|
|
22
|
-
def name
|
|
23
|
-
"n"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
register
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
end
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Idml
|
|
4
|
-
module Render
|
|
5
|
-
module PdfrbExt
|
|
6
|
-
# PDF `Do` operator: invoke an XObject (image or form) by name.
|
|
7
|
-
# pdfrb 0.3.0 doesn't include this operator; we register it here
|
|
8
|
-
# so Canvas#emit_op can draw images.
|
|
9
|
-
class InvokeXObject < Pdfrb::Content::Operator::Base
|
|
10
|
-
class << self
|
|
11
|
-
def name
|
|
12
|
-
"Do"
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def serialize(_serializer, xobject_name)
|
|
16
|
-
"/#{xobject_name} Do\n"
|
|
17
|
-
end
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
register
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
end
|
|
24
|
-
end
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Idml
|
|
4
|
-
module Render
|
|
5
|
-
# Extensions to the pdfrb gem for operators not yet implemented in
|
|
6
|
-
# pdfrb 0.3.0 (e.g., XObject invocation via `Do`).
|
|
7
|
-
module PdfrbExt
|
|
8
|
-
autoload :InvokeXObject, "#{__dir__}/pdfrb_ext/invoke_xobject"
|
|
9
|
-
autoload :Clip, "#{__dir__}/pdfrb_ext/clip"
|
|
10
|
-
autoload :EndPath, "#{__dir__}/pdfrb_ext/clip"
|
|
11
|
-
end
|
|
12
|
-
end
|
|
13
|
-
end
|
data/lib/idml/render/text.rb
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# DEPRECATED: This module is superseded by Canvas#text method.
|
|
4
|
-
# Text rendering is now handled by pdfrb's Canvas API.
|
|
5
|
-
|
|
6
|
-
module Idml
|
|
7
|
-
module Render
|
|
8
|
-
# Converts positioned glyphs to PDF text-showing operators.
|
|
9
|
-
module Text
|
|
10
|
-
module_function
|
|
11
|
-
|
|
12
|
-
# Begin text block
|
|
13
|
-
def begin_text
|
|
14
|
-
"BT"
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# End text block
|
|
18
|
-
def end_text
|
|
19
|
-
"ET"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Set font: `/FontName size Tf`
|
|
23
|
-
def set_font(font_name, size)
|
|
24
|
-
format("/%<name>s %<size>.1f Tf", name: font_name, size: size)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# Move text position: `x y Td`
|
|
28
|
-
def move_to(x, y)
|
|
29
|
-
format("%<x>.2f %<y>.2f Td", x: x, y: y)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Set text matrix: `a b c d e f Tm`
|
|
33
|
-
def set_matrix(a:, b:, c:, d:, e:, f:)
|
|
34
|
-
format("%<a>.4f %<b>.4f %<c>.4f %<d>.4f %<e>.2f %<f>.2f Tm",
|
|
35
|
-
a: a, b: b, c: c, d: d, e: e, f: f)
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Show a text string: `(escaped) Tj`
|
|
39
|
-
def show(text_string)
|
|
40
|
-
"(#{escape(text_string)}) Tj"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# Build a complete text-showing block for a run of glyphs
|
|
44
|
-
# at the same font/size, positioned at (x, y).
|
|
45
|
-
def show_run(text_string:, font_name:, size:, x:, y:)
|
|
46
|
-
[
|
|
47
|
-
begin_text,
|
|
48
|
-
set_font(font_name, size),
|
|
49
|
-
move_to(x, y),
|
|
50
|
-
show(text_string),
|
|
51
|
-
end_text,
|
|
52
|
-
].join("\n")
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
# Emit positioned glyphs. Groups glyphs by y coordinate into
|
|
56
|
-
# lines, sets the text matrix per-line for absolute positioning,
|
|
57
|
-
# then shows the line text. Multiple lines within one BT/ET block.
|
|
58
|
-
def show_positioned(glyphs)
|
|
59
|
-
return "" if glyphs.empty?
|
|
60
|
-
|
|
61
|
-
lines = group_by_line(glyphs)
|
|
62
|
-
lines.map do |line|
|
|
63
|
-
[
|
|
64
|
-
set_matrix(a: 1, b: 0, c: 0, d: 1,
|
|
65
|
-
e: line.first.x, f: line.first.y),
|
|
66
|
-
show(line.map { |g| [g.codepoint].pack("U") }.join),
|
|
67
|
-
].join("\n")
|
|
68
|
-
end.join("\n")
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def group_by_line(glyphs)
|
|
72
|
-
result = []
|
|
73
|
-
current = nil
|
|
74
|
-
glyphs.each do |glyph|
|
|
75
|
-
if current.nil? || current.first.y != glyph.y
|
|
76
|
-
current = []
|
|
77
|
-
result << current
|
|
78
|
-
end
|
|
79
|
-
current << glyph
|
|
80
|
-
end
|
|
81
|
-
result
|
|
82
|
-
end
|
|
83
|
-
private_class_method :group_by_line
|
|
84
|
-
|
|
85
|
-
# Escape characters that are special in PDF strings.
|
|
86
|
-
def escape(str)
|
|
87
|
-
str.to_s.gsub("\\", "\\\\\\\\").gsub("(", "\\(").gsub(")", "\\)")
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
end
|
|
91
|
-
end
|