idml 0.1.0 → 0.2.0
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/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/render/gradient_resolver.rb +9 -8
- data/lib/idml/render/image.rb +5 -10
- data/lib/idml/render/pdfrb_writer.rb +36 -58
- data/lib/idml/render/spread_renderer.rb +7 -5
- data/lib/idml/render.rb +13 -22
- 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
data/lib/idml/render/color.rb
DELETED
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# DEPRECATED: This module is superseded by ColorHelper + Canvas
|
|
4
|
-
# fill_color/stroke_color methods. Kept for backward compatibility.
|
|
5
|
-
|
|
6
|
-
module Idml
|
|
7
|
-
module Render
|
|
8
|
-
# Converts IDML color values to PDF content-stream operators.
|
|
9
|
-
module Color
|
|
10
|
-
module_function
|
|
11
|
-
|
|
12
|
-
# RGB fill: `r g b rg`. Values 0.0–1.0.
|
|
13
|
-
def fill_rgb(r, g, b)
|
|
14
|
-
format("%<r>.4f %<g>.4f %<b>.4f rg", r: r, g: g, b: b)
|
|
15
|
-
end
|
|
16
|
-
|
|
17
|
-
# RGB stroke: `r g b RG`.
|
|
18
|
-
def stroke_rgb(r, g, b)
|
|
19
|
-
format("%<r>.4f %<g>.4f %<b>.4f RG", r: r, g: g, b: b)
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# CMYK fill: `c m y k k`.
|
|
23
|
-
def fill_cmyk(c, m, y, k)
|
|
24
|
-
format("%<c>.4f %<m>.4f %<y>.4f %<k>.4f k", c: c, m: m, y: y, k: k)
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
# CMYK stroke: `c m y k K`.
|
|
28
|
-
def stroke_cmyk(c, m, y, k)
|
|
29
|
-
format("%<c>.4f %<m>.4f %<y>.4f %<k>.4f K", c: c, m: m, y: y, k: k)
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Parse IDML color space value (space-separated 0–255 or 0–100
|
|
33
|
-
# depending on model) to normalized 0.0–1.0.
|
|
34
|
-
def normalize_channel(value, max = 255)
|
|
35
|
-
value.to_f / max
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
# Black fill (common default).
|
|
39
|
-
def black_fill
|
|
40
|
-
"0 g"
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
# White fill.
|
|
44
|
-
def white_fill
|
|
45
|
-
"1 g"
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
# Dispatch: accepts a color hash ({ model:, r:, g:, b: } or
|
|
49
|
-
# { model:, c:, m:, y:, k: }) and returns the appropriate
|
|
50
|
-
# fill operator string.
|
|
51
|
-
def fill_op(color)
|
|
52
|
-
case color[:model]
|
|
53
|
-
when :rgb then fill_rgb(color[:r], color[:g], color[:b])
|
|
54
|
-
when :cmyk then fill_cmyk(color[:c], color[:m], color[:y], color[:k])
|
|
55
|
-
end
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
# Dispatch: accepts a color hash and returns the appropriate
|
|
59
|
-
# stroke operator string.
|
|
60
|
-
def stroke_op(color)
|
|
61
|
-
case color[:model]
|
|
62
|
-
when :rgb then stroke_rgb(color[:r], color[:g], color[:b])
|
|
63
|
-
when :cmyk then stroke_cmyk(color[:c], color[:m], color[:y],
|
|
64
|
-
color[:k])
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
end
|
|
69
|
-
end
|
|
@@ -1,79 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# DEPRECATED: This module is superseded by PdfrbWriter.register_font.
|
|
4
|
-
# Font embedding is now handled by pdfrb (Pdfrb::Document#fonts.add).
|
|
5
|
-
|
|
6
|
-
module Idml
|
|
7
|
-
module Render
|
|
8
|
-
# Generates PDF font structures for embedded TrueType fonts.
|
|
9
|
-
# Produces the Widths array (WinAnsiEncoding 32–255), FontDescriptor
|
|
10
|
-
# metadata, and FontFile2 binary embedding.
|
|
11
|
-
module FontEmbedder
|
|
12
|
-
FIRST_CHAR = 32
|
|
13
|
-
LAST_CHAR = 255
|
|
14
|
-
|
|
15
|
-
# PDF WinAnsiEncoding = Code Page 1252. Bytes 0x80-0x9F differ
|
|
16
|
-
# from ISO-8859-1 and map to specific Unicode codepoints.
|
|
17
|
-
CP1252_SPECIAL = {
|
|
18
|
-
0x80 => 0x20AC, 0x82 => 0x201A, 0x83 => 0x0192, 0x84 => 0x201E,
|
|
19
|
-
0x85 => 0x2026, 0x86 => 0x2020, 0x87 => 0x2021, 0x88 => 0x02C6,
|
|
20
|
-
0x89 => 0x2030, 0x8A => 0x0160, 0x8B => 0x2039, 0x8C => 0x0152,
|
|
21
|
-
0x8E => 0x017D, 0x91 => 0x2018, 0x92 => 0x2019, 0x93 => 0x201C,
|
|
22
|
-
0x94 => 0x201D, 0x95 => 0x2022, 0x96 => 0x2013, 0x97 => 0x2014,
|
|
23
|
-
0x98 => 0x02DC, 0x99 => 0x2122, 0x9A => 0x0161, 0x9B => 0x203A,
|
|
24
|
-
0x9C => 0x0153, 0x9E => 0x017E, 0x9F => 0x0178
|
|
25
|
-
}.freeze
|
|
26
|
-
|
|
27
|
-
private_constant :CP1252_SPECIAL
|
|
28
|
-
|
|
29
|
-
module_function
|
|
30
|
-
|
|
31
|
-
# Widths array for WinAnsiEncoding (chars 32–255), in PDF text
|
|
32
|
-
# units (1000 units per em).
|
|
33
|
-
def widths_array(font_metrics)
|
|
34
|
-
(FIRST_CHAR..LAST_CHAR).map do |byte|
|
|
35
|
-
cp = winansi_to_unicode(byte)
|
|
36
|
-
raw = font_metrics.glyph_width(cp)
|
|
37
|
-
scale_to_text_units(raw, font_metrics.units_per_em)
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
# Map a WinAnsi (CP1252) byte value to its Unicode codepoint.
|
|
42
|
-
def winansi_to_unicode(byte)
|
|
43
|
-
CP1252_SPECIAL.fetch(byte, byte)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
def scale_to_text_units(font_units, upem)
|
|
47
|
-
(font_units.to_f * 1000 / upem).round
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
# FontDescriptor metadata hash.
|
|
51
|
-
def descriptor(font_metrics)
|
|
52
|
-
upem = font_metrics.units_per_em
|
|
53
|
-
scale = 1000.0 / upem
|
|
54
|
-
ascent = font_metrics.ascent.to_f
|
|
55
|
-
descent = font_metrics.descent.to_f
|
|
56
|
-
{
|
|
57
|
-
font_name: font_metrics.postscript_name,
|
|
58
|
-
flags: 32,
|
|
59
|
-
font_bbox: [
|
|
60
|
-
(descent * scale).round,
|
|
61
|
-
0,
|
|
62
|
-
1000,
|
|
63
|
-
(ascent * scale).round,
|
|
64
|
-
],
|
|
65
|
-
italic_angle: 0,
|
|
66
|
-
ascent: (ascent * scale).round,
|
|
67
|
-
descent: (descent * scale).round,
|
|
68
|
-
cap_height: (ascent * scale * 0.7).round,
|
|
69
|
-
stem_v: 80,
|
|
70
|
-
}
|
|
71
|
-
end
|
|
72
|
-
|
|
73
|
-
# Read raw font binary data from the FontMetrics source path.
|
|
74
|
-
def raw_font_data(font_metrics)
|
|
75
|
-
File.binread(font_metrics.path)
|
|
76
|
-
end
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
data/lib/idml/render/path.rb
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# DEPRECATED: This module is superseded by Pdfrb::Content::Canvas
|
|
4
|
-
# drawing methods (rectangle, stroke, fill, etc.).
|
|
5
|
-
|
|
6
|
-
module Idml
|
|
7
|
-
module Render
|
|
8
|
-
# Converts IDML geometric page items to PDF path operators.
|
|
9
|
-
module Path
|
|
10
|
-
module_function
|
|
11
|
-
|
|
12
|
-
# Rectangle → `x y w h re`
|
|
13
|
-
def rectangle(x:, y:, width:, height:)
|
|
14
|
-
format("%<x>.2f %<y>.2f %<w>.2f %<h>.2f re", x: x, y: y, w: width,
|
|
15
|
-
h: height)
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
# Save graphics state
|
|
19
|
-
def save_state
|
|
20
|
-
"q"
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
# Restore graphics state
|
|
24
|
-
def restore_state
|
|
25
|
-
"Q"
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
# Transformation matrix: `a b c d e f cm`
|
|
29
|
-
def transform(a:, b:, c:, d:, e:, f:)
|
|
30
|
-
format("%<a>.4f %<b>.4f %<c>.4f %<d>.4f %<e>.2f %<f>.2f cm",
|
|
31
|
-
a: a, b: b, c: c, d: d, e: e, f: f)
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
# Fill the current path (nonzero winding).
|
|
35
|
-
def fill
|
|
36
|
-
"f"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
# Set clip path (nonzero winding). Must be followed by `n` (no-op).
|
|
40
|
-
def clip
|
|
41
|
-
"W n"
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
# Set clip path (even-odd).
|
|
45
|
-
def eo_clip
|
|
46
|
-
"W* n"
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
# Stroke the current path.
|
|
50
|
-
def stroke
|
|
51
|
-
"S"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# Set stroke line width: `w w`
|
|
55
|
-
def stroke_width(weight)
|
|
56
|
-
format("%<w>.2f w", w: weight)
|
|
57
|
-
end
|
|
58
|
-
|
|
59
|
-
# Close, fill, and stroke the current path.
|
|
60
|
-
def fill_stroke
|
|
61
|
-
"B"
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
# Close the current path.
|
|
65
|
-
def close
|
|
66
|
-
"h"
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
# Move to a point: `x y m`
|
|
70
|
-
def move_to(x, y)
|
|
71
|
-
format("%<x>.2f %<y>.2f m", x: x, y: y)
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
# Line to a point: `x y l`
|
|
75
|
-
def line_to(x, y)
|
|
76
|
-
format("%<x>.2f %<y>.2f l", x: x, y: y)
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
end
|
|
80
|
-
end
|
|
@@ -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
|