idml 0.2.9 → 0.3.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.lock +4 -20
- data/TODO.pdf/63-replace-fontmetrics-with-pdfrb.md +59 -58
- data/TODO.pdf/65-pdfrb-019-integration.md +57 -29
- data/TODO.pdf/67-text-rich-multi-run.md +35 -39
- data/TODO.pdf/76-tagged-pdf-structure.md +63 -87
- data/TODO.pdf/77-pdfa-xmp-output-intent.md +51 -59
- data/TODO.pdf/78-hyperlinks.md +79 -0
- data/TODO.pdf/79-bookmarks-outline.md +72 -0
- data/idml.gemspec +0 -1
- data/lib/idml/render/image_collector.rb +107 -0
- data/lib/idml/render/page_item_renderer.rb +18 -1
- data/lib/idml/render/pdfa_packet.rb +135 -0
- data/lib/idml/render/pdfrb_writer.rb +2 -1
- data/lib/idml/render/pipeline.rb +47 -101
- data/lib/idml/render/render_context.rb +3 -1
- data/lib/idml/render/renderers/group_renderer.rb +3 -1
- data/lib/idml/render/renderers/text_frame_renderer.rb +34 -25
- data/lib/idml/render/spread_renderer.rb +9 -5
- data/lib/idml/render/structure_mapper.rb +54 -0
- data/lib/idml/render/structure_tracker.rb +55 -0
- data/lib/idml/render.rb +4 -0
- data/lib/idml/text_engine/pdfrb_font_metrics.rb +81 -0
- data/lib/idml/text_engine.rb +1 -2
- data/lib/idml/version.rb +1 -1
- metadata +8 -17
- data/lib/idml/text_engine/font_metrics.rb +0 -226
- data/lib/idml/text_engine/font_resolver.rb +0 -105
data/lib/idml/render/pipeline.rb
CHANGED
|
@@ -13,7 +13,7 @@ module Idml
|
|
|
13
13
|
compliance: nil, tagged: false, subset_fonts: true)
|
|
14
14
|
@package = package
|
|
15
15
|
@output_path = output_path
|
|
16
|
-
@
|
|
16
|
+
@font_search_paths = font_search_paths
|
|
17
17
|
@compliance = compliance
|
|
18
18
|
@tagged = tagged
|
|
19
19
|
@subset_fonts = subset_fonts
|
|
@@ -21,18 +21,26 @@ module Idml
|
|
|
21
21
|
|
|
22
22
|
def call
|
|
23
23
|
writer = PdfrbWriter.new
|
|
24
|
-
|
|
24
|
+
metadata = combined_metadata
|
|
25
|
+
writer.set_info(metadata)
|
|
25
26
|
writer.enable_tagged if @tagged
|
|
27
|
+
PdfaPacket.attach(writer.document, metadata) if pdfa_requested?
|
|
28
|
+
structure = StructureTracker.new(enabled: @tagged)
|
|
26
29
|
layer_filter = LayerFilter.from_designmap(@package.designmap)
|
|
27
30
|
font_ref_resolver = FontReferenceResolver.build(@package)
|
|
28
31
|
base_dir = File.dirname(@package.path)
|
|
29
|
-
|
|
32
|
+
font_resource = register_font(writer)
|
|
33
|
+
font_metrics = build_font_metrics(writer, font_resource)
|
|
34
|
+
page_index = -1
|
|
30
35
|
|
|
31
36
|
@package.spreads.each do |spread|
|
|
32
|
-
render_spread_pages(writer, spread, base_dir,
|
|
33
|
-
|
|
37
|
+
page_index = render_spread_pages(writer, spread, base_dir,
|
|
38
|
+
layer_filter, font_ref_resolver,
|
|
39
|
+
font_resource, font_metrics,
|
|
40
|
+
structure, page_index)
|
|
34
41
|
end
|
|
35
42
|
|
|
43
|
+
structure.flush(writer)
|
|
36
44
|
writer.build_structure if @tagged
|
|
37
45
|
writer.subset_fonts! if @subset_fonts
|
|
38
46
|
writer.write(@output_path)
|
|
@@ -41,126 +49,58 @@ module Idml
|
|
|
41
49
|
|
|
42
50
|
private
|
|
43
51
|
|
|
52
|
+
def pdfa_requested?
|
|
53
|
+
@compliance&.to_s&.start_with?("pdfa")
|
|
54
|
+
end
|
|
55
|
+
|
|
44
56
|
def render_spread_pages(writer, spread, base_dir, layer_filter,
|
|
45
|
-
font_ref_resolver,
|
|
57
|
+
font_ref_resolver, font_resource, font_metrics,
|
|
58
|
+
structure, page_offset)
|
|
46
59
|
pages = spread.spread.flat_map(&:page)
|
|
47
|
-
image_refs =
|
|
48
|
-
|
|
60
|
+
image_refs = ImageCollector.new(writer: writer, base_dir: base_dir,
|
|
61
|
+
page_height: DEFAULT_HEIGHT).collect(spread)
|
|
62
|
+
renderer = build_renderer(layer_filter, font_ref_resolver,
|
|
63
|
+
font_resource, font_metrics, structure: structure)
|
|
64
|
+
current = page_offset
|
|
49
65
|
|
|
50
66
|
pages.each do |page|
|
|
67
|
+
current += 1
|
|
51
68
|
dims = page_dimensions_for(page)
|
|
52
69
|
canvas = writer.add_page(width: dims[:width], height: dims[:height])
|
|
53
70
|
renderer.render(canvas, spread, page_width: dims[:width],
|
|
54
71
|
page_height: dims[:height],
|
|
55
|
-
image_refs: image_refs
|
|
72
|
+
image_refs: image_refs,
|
|
73
|
+
page_index: current)
|
|
56
74
|
end
|
|
75
|
+
current
|
|
57
76
|
end
|
|
58
77
|
|
|
59
|
-
def build_renderer(layer_filter, font_ref_resolver,
|
|
78
|
+
def build_renderer(layer_filter, font_ref_resolver, font_resource,
|
|
79
|
+
font_metrics, structure:)
|
|
60
80
|
SpreadRenderer.new(
|
|
61
|
-
|
|
62
|
-
font_ps_name:
|
|
81
|
+
font_metrics: font_metrics,
|
|
82
|
+
font_ps_name: font_resource,
|
|
63
83
|
package: @package,
|
|
64
84
|
layer_filter: layer_filter,
|
|
65
85
|
font_ref_resolver: font_ref_resolver,
|
|
86
|
+
structure: structure,
|
|
66
87
|
)
|
|
67
88
|
end
|
|
68
89
|
|
|
69
|
-
def collect_images(writer, spread, base_dir, layer_filter)
|
|
70
|
-
refs = []
|
|
71
|
-
spread.each_page_item do |item|
|
|
72
|
-
next unless layer_filter.visible?(item)
|
|
73
|
-
|
|
74
|
-
images_for_item(item).each do |img|
|
|
75
|
-
ref = register_image(writer, img, item, base_dir)
|
|
76
|
-
refs << ref if ref
|
|
77
|
-
end
|
|
78
|
-
end
|
|
79
|
-
refs
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def images_for_item(item)
|
|
83
|
-
case item
|
|
84
|
-
when Idml::Elements::Rectangle, Idml::Elements::Polygon
|
|
85
|
-
item.image
|
|
86
|
-
else
|
|
87
|
-
[]
|
|
88
|
-
end
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
def register_image(writer, image, parent, base_dir)
|
|
92
|
-
uri = image.resource_uri
|
|
93
|
-
return nil unless uri
|
|
94
|
-
|
|
95
|
-
existing = writer.image_name_for(uri)
|
|
96
|
-
return reuse_image(writer, existing, image, parent) if existing
|
|
97
|
-
|
|
98
|
-
load_new_image(writer, image, parent, base_dir, uri)
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
def reuse_image(_writer, name, image, parent)
|
|
102
|
-
{ name: name, placement: compute_placement(image, parent),
|
|
103
|
-
clip_box: parent_clip_box(parent) }
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def load_new_image(writer, image, parent, base_dir, uri)
|
|
107
|
-
path = Image.resolve_path(uri, base_dir: base_dir)
|
|
108
|
-
return nil unless File.exist?(path)
|
|
109
|
-
|
|
110
|
-
data = File.binread(path)
|
|
111
|
-
dims = image_dimensions(data)
|
|
112
|
-
return nil unless dims
|
|
113
|
-
|
|
114
|
-
name = writer.add_image(data: data)
|
|
115
|
-
writer.register_image_name(uri, name)
|
|
116
|
-
{ name: name, placement: compute_placement(image, parent, dims[1]),
|
|
117
|
-
clip_box: parent_clip_box(parent) }
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def parent_clip_box(parent)
|
|
121
|
-
return nil unless parent.geometric_bounds
|
|
122
|
-
|
|
123
|
-
Geometry.placement_rect(parent.geometric_bounds,
|
|
124
|
-
parent.item_transform, DEFAULT_HEIGHT)
|
|
125
|
-
end
|
|
126
|
-
|
|
127
|
-
def image_dimensions(data)
|
|
128
|
-
format = Image.detect_format(data)
|
|
129
|
-
return nil unless format
|
|
130
|
-
|
|
131
|
-
if format == :png
|
|
132
|
-
Image.png_dimensions(data)
|
|
133
|
-
else
|
|
134
|
-
Image.jpeg_dimensions(data)
|
|
135
|
-
end
|
|
136
|
-
end
|
|
137
|
-
|
|
138
|
-
def compute_placement(image, parent, pixel_height = 100)
|
|
139
|
-
Image.compute_placement(
|
|
140
|
-
image_transform: parse_transform_safe(image.item_transform),
|
|
141
|
-
parent_transform: parse_transform_safe(parent.item_transform),
|
|
142
|
-
pixel_height: pixel_height,
|
|
143
|
-
page_height: DEFAULT_HEIGHT,
|
|
144
|
-
)
|
|
145
|
-
end
|
|
146
|
-
|
|
147
|
-
def parse_transform_safe(str)
|
|
148
|
-
Image.parse_transform(str) || Image.identity
|
|
149
|
-
end
|
|
150
|
-
|
|
151
90
|
def page_dimensions_for(page)
|
|
152
91
|
{ width: page.width || DEFAULT_WIDTH,
|
|
153
92
|
height: page.height || DEFAULT_HEIGHT }
|
|
154
93
|
end
|
|
155
94
|
|
|
156
|
-
def
|
|
157
|
-
|
|
158
|
-
|
|
95
|
+
def build_font_metrics(writer, font_resource)
|
|
96
|
+
return nil if font_resource == Render::DEFAULT_FONT
|
|
97
|
+
|
|
98
|
+
TextEngine::PdfrbFontMetrics.new(writer.document.fonts, font_resource)
|
|
99
|
+
rescue StandardError
|
|
100
|
+
nil
|
|
159
101
|
end
|
|
160
102
|
|
|
161
103
|
def register_font(writer)
|
|
162
|
-
return Render::DEFAULT_FONT unless @font_resolver
|
|
163
|
-
|
|
164
104
|
path = resolve_document_font_path
|
|
165
105
|
return Render::DEFAULT_FONT unless path
|
|
166
106
|
|
|
@@ -169,6 +109,12 @@ module Idml
|
|
|
169
109
|
Render::DEFAULT_FONT
|
|
170
110
|
end
|
|
171
111
|
|
|
112
|
+
def font_resolver
|
|
113
|
+
@font_resolver ||= Pdfrb::FontResolver.new(
|
|
114
|
+
search_paths: @font_search_paths || Pdfrb::FontResolver::DEFAULT_SEARCH_PATHS,
|
|
115
|
+
)
|
|
116
|
+
end
|
|
117
|
+
|
|
172
118
|
def resolve_document_font_path
|
|
173
119
|
return nil unless @package&.fonts
|
|
174
120
|
|
|
@@ -184,8 +130,8 @@ module Idml
|
|
|
184
130
|
next unless font.post_script_name
|
|
185
131
|
next if font.status == "Missing"
|
|
186
132
|
|
|
187
|
-
|
|
188
|
-
return
|
|
133
|
+
path = font_resolver.find_by_ps_name(font.post_script_name)
|
|
134
|
+
return path if path
|
|
189
135
|
end
|
|
190
136
|
nil
|
|
191
137
|
end
|
|
@@ -9,13 +9,15 @@ module Idml
|
|
|
9
9
|
RenderContext = Struct.new(
|
|
10
10
|
:item,
|
|
11
11
|
:package,
|
|
12
|
-
:
|
|
12
|
+
:font_metrics,
|
|
13
13
|
:font_ref_resolver,
|
|
14
14
|
:color_resolver,
|
|
15
15
|
:font_ps_name,
|
|
16
16
|
:page_width,
|
|
17
17
|
:page_height,
|
|
18
18
|
:layer_filter,
|
|
19
|
+
:structure,
|
|
20
|
+
:page_index,
|
|
19
21
|
keyword_init: true,
|
|
20
22
|
)
|
|
21
23
|
end
|
|
@@ -14,13 +14,15 @@ module Idml
|
|
|
14
14
|
child_context = Render::RenderContext.new(
|
|
15
15
|
item: child,
|
|
16
16
|
package: context.package,
|
|
17
|
-
|
|
17
|
+
font_metrics: context.font_metrics,
|
|
18
18
|
font_ref_resolver: context.font_ref_resolver,
|
|
19
19
|
color_resolver: context.color_resolver,
|
|
20
20
|
font_ps_name: context.font_ps_name,
|
|
21
21
|
page_width: context.page_width,
|
|
22
22
|
page_height: context.page_height,
|
|
23
23
|
layer_filter: context.layer_filter,
|
|
24
|
+
structure: context.structure,
|
|
25
|
+
page_index: context.page_index,
|
|
24
26
|
)
|
|
25
27
|
PageItemRenderer.render(canvas, child_context)
|
|
26
28
|
end
|
|
@@ -4,10 +4,10 @@ module Idml
|
|
|
4
4
|
module Render
|
|
5
5
|
module Renderers
|
|
6
6
|
# Renders an IDML TextFrame on a Pdfrb::Content::Canvas. Extracts
|
|
7
|
-
# styled runs via StyleResolver. When a FontMetrics is available
|
|
8
|
-
# runs the full text engine pipeline (Shaper →
|
|
9
|
-
#
|
|
10
|
-
#
|
|
7
|
+
# styled runs via StyleResolver. When a FontMetrics is available
|
|
8
|
+
# (pdfrb-backed), runs the full text engine pipeline (Shaper →
|
|
9
|
+
# LineBreaker) for proper word-wrap; otherwise falls back to a
|
|
10
|
+
# single text_rich call per frame.
|
|
11
11
|
class TextFrameRenderer
|
|
12
12
|
DEFAULT_SIZE = 12.0
|
|
13
13
|
LEADING_FACTOR = 1.2
|
|
@@ -28,7 +28,7 @@ module Idml
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
def self.render_text(canvas, runs, context, box)
|
|
31
|
-
font =
|
|
31
|
+
font = context.font_metrics
|
|
32
32
|
if font
|
|
33
33
|
engine_render(canvas, runs, context, box, font)
|
|
34
34
|
else
|
|
@@ -47,26 +47,6 @@ module Idml
|
|
|
47
47
|
end
|
|
48
48
|
private_class_method :frame_box
|
|
49
49
|
|
|
50
|
-
def self.resolve_font_metrics(context)
|
|
51
|
-
return nil unless context.font_resolver
|
|
52
|
-
|
|
53
|
-
context.font_resolver.resolve(
|
|
54
|
-
family_name: Render::DEFAULT_FONT, style_name: "Regular",
|
|
55
|
-
)
|
|
56
|
-
end
|
|
57
|
-
private_class_method :resolve_font_metrics
|
|
58
|
-
|
|
59
|
-
def self.simple_render(canvas, runs, context, box)
|
|
60
|
-
runs.each_with_index do |run, index|
|
|
61
|
-
y = box[:y] + box[:height] -
|
|
62
|
-
((index + 1) * run.point_size * LEADING_FACTOR)
|
|
63
|
-
canvas.text(run.text, at: [box[:x], y],
|
|
64
|
-
font: context.font_ps_name,
|
|
65
|
-
size: run.point_size)
|
|
66
|
-
end
|
|
67
|
-
end
|
|
68
|
-
private_class_method :simple_render
|
|
69
|
-
|
|
70
50
|
def self.engine_render(canvas, runs, context, box, font)
|
|
71
51
|
baseline_y = box[:y] + box[:height] - runs.first.point_size
|
|
72
52
|
|
|
@@ -78,6 +58,9 @@ module Idml
|
|
|
78
58
|
end
|
|
79
59
|
private_class_method :engine_render
|
|
80
60
|
|
|
61
|
+
# Emits one `canvas.text_lines` call per run, after shaping
|
|
62
|
+
# and line-breaking with real font metrics. Lines that fall
|
|
63
|
+
# below the frame's bottom edge are clipped.
|
|
81
64
|
def self.render_run_lines(canvas, run, context, box, font, baseline_y)
|
|
82
65
|
size = run.point_size
|
|
83
66
|
glyphs = TextEngine::Shaper.shape(
|
|
@@ -105,6 +88,32 @@ module Idml
|
|
|
105
88
|
baseline_y
|
|
106
89
|
end
|
|
107
90
|
private_class_method :render_run_lines
|
|
91
|
+
|
|
92
|
+
# Fallback when no metrics are available: emit all runs as
|
|
93
|
+
# one `text_rich` block, letting pdfrb measure advance widths.
|
|
94
|
+
# Uses pdfrb's measurement API directly (no Fontisan).
|
|
95
|
+
def self.simple_render(canvas, runs, context, box)
|
|
96
|
+
runs_for = build_rich_runs(runs, context)
|
|
97
|
+
return if runs_for.empty?
|
|
98
|
+
|
|
99
|
+
first_size = runs.first.point_size
|
|
100
|
+
canvas.text_rich(
|
|
101
|
+
runs_for,
|
|
102
|
+
at: [box[:x], box[:y] + box[:height] - first_size],
|
|
103
|
+
)
|
|
104
|
+
end
|
|
105
|
+
private_class_method :simple_render
|
|
106
|
+
|
|
107
|
+
def self.build_rich_runs(runs, context)
|
|
108
|
+
runs.map do |run|
|
|
109
|
+
{
|
|
110
|
+
text: run.text,
|
|
111
|
+
font: context.font_ps_name,
|
|
112
|
+
size: run.point_size,
|
|
113
|
+
}
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
private_class_method :build_rich_runs
|
|
108
117
|
end
|
|
109
118
|
end
|
|
110
119
|
end
|
|
@@ -4,26 +4,30 @@ module Idml
|
|
|
4
4
|
module Render
|
|
5
5
|
# Renders a typed `Parts::Spread` onto a Pdfrb::Content::Canvas.
|
|
6
6
|
class SpreadRenderer
|
|
7
|
-
def initialize(
|
|
7
|
+
def initialize(font_metrics: nil, font_ps_name: Render::DEFAULT_FONT,
|
|
8
8
|
package: nil, layer_filter: LayerFilter::EXCLUDE_NONE,
|
|
9
|
-
font_ref_resolver: nil)
|
|
10
|
-
@
|
|
9
|
+
font_ref_resolver: nil, structure: nil)
|
|
10
|
+
@font_metrics = font_metrics
|
|
11
11
|
@font_ps_name = font_ps_name
|
|
12
12
|
@package = package
|
|
13
13
|
@layer_filter = layer_filter
|
|
14
14
|
@font_ref_resolver = font_ref_resolver
|
|
15
|
+
@structure = structure
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
def render(canvas, spread, page_width:, page_height:, image_refs: []
|
|
18
|
+
def render(canvas, spread, page_width:, page_height:, image_refs: [],
|
|
19
|
+
page_index: 0)
|
|
18
20
|
context_base = {
|
|
19
21
|
package: @package,
|
|
20
|
-
|
|
22
|
+
font_metrics: @font_metrics,
|
|
21
23
|
font_ref_resolver: @font_ref_resolver,
|
|
22
24
|
color_resolver: build_color_resolver,
|
|
23
25
|
font_ps_name: @font_ps_name,
|
|
24
26
|
page_width: page_width,
|
|
25
27
|
page_height: page_height,
|
|
26
28
|
layer_filter: @layer_filter,
|
|
29
|
+
structure: @structure,
|
|
30
|
+
page_index: page_index,
|
|
27
31
|
}
|
|
28
32
|
|
|
29
33
|
canvas.save_graphics_state do
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
# Maps IDML page-item classes to PDF structure types (PDF 1.7
|
|
6
|
+
# §14.8.4). Each visible page item becomes one structure element
|
|
7
|
+
# when tagged output is enabled.
|
|
8
|
+
#
|
|
9
|
+
# Mapping rationale:
|
|
10
|
+
# TextFrame → P (paragraph)
|
|
11
|
+
# Group → Sect (section, contains children)
|
|
12
|
+
# Table → Table
|
|
13
|
+
# Rectangle/Polygon with image → Figure
|
|
14
|
+
# Rectangle/Polygon without image → Path (decorative shape)
|
|
15
|
+
# GraphicLine → Path
|
|
16
|
+
#
|
|
17
|
+
# The PDF spec reserves lowercase for standard types; pdfrb's
|
|
18
|
+
# `add_element(type:)` accepts a Symbol which is serialised as
|
|
19
|
+
# `/Type`. We use Title-case Symbols to match the spec's typical
|
|
20
|
+
# spelling (P, Sect, Table, Figure, Path).
|
|
21
|
+
module StructureMapper
|
|
22
|
+
def self.type_for(item)
|
|
23
|
+
case item
|
|
24
|
+
when Idml::Elements::TextFrame then :P
|
|
25
|
+
when Idml::Elements::Group then :Sect
|
|
26
|
+
when Idml::Elements::Table then :Table
|
|
27
|
+
when Idml::Elements::GraphicLine then :Path
|
|
28
|
+
when Idml::Elements::Rectangle, Idml::Elements::Polygon
|
|
29
|
+
shape_type(item)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.shape_type(item)
|
|
34
|
+
image?(item) ? :Figure : :Path
|
|
35
|
+
end
|
|
36
|
+
private_class_method :shape_type
|
|
37
|
+
|
|
38
|
+
# Optional `Alt` text for Figure elements. IDML doesn't have a
|
|
39
|
+
# dedicated alt-text attribute on `<Image>`, but the parent
|
|
40
|
+
# page item's `Name` (if present) is a useful fallback.
|
|
41
|
+
def self.alt_for(item)
|
|
42
|
+
case item
|
|
43
|
+
when Idml::Elements::Rectangle, Idml::Elements::Polygon
|
|
44
|
+
image?(item) ? item.name : nil
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.image?(item)
|
|
49
|
+
Array(item.image).any?
|
|
50
|
+
end
|
|
51
|
+
private_class_method :image?
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module Render
|
|
5
|
+
# Allocates MCID integers per page and buffers structure-element
|
|
6
|
+
# registrations until `Pipeline` is ready to flush them. Created
|
|
7
|
+
# once per pipeline run; `enabled?` is false when `tagged:` was
|
|
8
|
+
# not requested, in which case every method is a no-op.
|
|
9
|
+
class StructureTracker
|
|
10
|
+
def initialize(enabled: false)
|
|
11
|
+
@enabled = enabled
|
|
12
|
+
@entries = []
|
|
13
|
+
@page_mcids = Hash.new(0)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def enabled?
|
|
17
|
+
@enabled
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
# Returns the next MCID for the given page index. MCIDs are
|
|
21
|
+
# per-page in PDF (reset to 0 on each new page).
|
|
22
|
+
def next_mcid(page_index)
|
|
23
|
+
current = @page_mcids[page_index]
|
|
24
|
+
@page_mcids[page_index] = current + 1
|
|
25
|
+
current
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def add(type, page_index:, mcid:, text: nil, alt: nil)
|
|
29
|
+
return unless @enabled
|
|
30
|
+
|
|
31
|
+
@entries << Entry.new(type, page_index, mcid, text, alt)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Push buffered entries into the writer in registration order.
|
|
35
|
+
def flush(writer)
|
|
36
|
+
return unless @enabled
|
|
37
|
+
|
|
38
|
+
@entries.each do |entry|
|
|
39
|
+
writer.add_structure_element(entry.type, page_index: entry.page_index,
|
|
40
|
+
mcid: entry.mcid,
|
|
41
|
+
text: entry.text,
|
|
42
|
+
alt: entry.alt)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
Entry = Struct.new(:type, :page_index, :mcid, :text, :alt, keyword_init: true) do
|
|
47
|
+
def initialize(type, page_index, mcid, text, alt)
|
|
48
|
+
super(type: type, page_index: page_index, mcid: mcid,
|
|
49
|
+
text: text, alt: alt)
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
private_constant :Entry
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
data/lib/idml/render.rb
CHANGED
|
@@ -15,6 +15,10 @@ module Idml
|
|
|
15
15
|
autoload :StrokeStyle, "#{__dir__}/render/stroke_style"
|
|
16
16
|
autoload :LayerFilter, "#{__dir__}/render/layer_filter"
|
|
17
17
|
autoload :Placement, "#{__dir__}/render/placement"
|
|
18
|
+
autoload :ImageCollector, "#{__dir__}/render/image_collector"
|
|
19
|
+
autoload :StructureTracker, "#{__dir__}/render/structure_tracker"
|
|
20
|
+
autoload :StructureMapper, "#{__dir__}/render/structure_mapper"
|
|
21
|
+
autoload :PdfaPacket, "#{__dir__}/render/pdfa_packet"
|
|
18
22
|
autoload :StoryThreader, "#{__dir__}/render/story_threader"
|
|
19
23
|
autoload :PdfrbWriter, "#{__dir__}/render/pdfrb_writer"
|
|
20
24
|
autoload :SpreadRenderer, "#{__dir__}/render/spread_renderer"
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Idml
|
|
4
|
+
module TextEngine
|
|
5
|
+
# Adapter that exposes the same measurement surface as
|
|
6
|
+
# `FontMetrics` (the Fontisan-based parser) but delegates to
|
|
7
|
+
# pdfrb's `Fonts` collection + a registered font resource Symbol.
|
|
8
|
+
#
|
|
9
|
+
# Once a font is registered with the pdfrb document via
|
|
10
|
+
# `document.fonts.add(path)`, pdfrb parses the TTF tables (cmap,
|
|
11
|
+
# hmtx, head, hhea) and exposes real per-glyph advance widths
|
|
12
|
+
# through `Fonts#glyph_width(resource, codepoint)` and
|
|
13
|
+
# `Fonts#metrics_for(resource)`. This adapter wraps that pair so
|
|
14
|
+
# the text engine (Shaper, LineBreaker) can use real metrics
|
|
15
|
+
# without any Fontisan dependency.
|
|
16
|
+
class PdfrbFontMetrics
|
|
17
|
+
attr_reader :resource, :fonts
|
|
18
|
+
|
|
19
|
+
def initialize(fonts_collection, resource)
|
|
20
|
+
@fonts = fonts_collection
|
|
21
|
+
@resource = resource
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def units_per_em
|
|
25
|
+
metrics_data[:units_per_em] || 1000
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def ascent
|
|
29
|
+
metrics_data[:ascent] || 800
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def descent
|
|
33
|
+
metrics_data[:descent] || -200
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def line_gap
|
|
37
|
+
0
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Returns raw advance width in font units (not scaled by size).
|
|
41
|
+
def glyph_width(codepoint)
|
|
42
|
+
cp = codepoint.is_a?(String) ? codepoint.each_codepoint.first : codepoint
|
|
43
|
+
@fonts.glyph_width(@resource, cp).to_i
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def measure_text(text, size:)
|
|
47
|
+
return 0.0 if text.nil? || text.empty?
|
|
48
|
+
|
|
49
|
+
text.each_codepoint.sum do |cp|
|
|
50
|
+
glyph_width(cp) * size.to_f / units_per_em
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def kerning_pair(_left_cp, _right_cp)
|
|
55
|
+
0
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def postscript_name
|
|
59
|
+
@resource.to_s
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def family_name
|
|
63
|
+
postscript_name
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def style_name
|
|
67
|
+
"Regular"
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def path
|
|
71
|
+
nil
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
private
|
|
75
|
+
|
|
76
|
+
def metrics_data
|
|
77
|
+
@fonts.metrics_for(@resource) || {}
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
data/lib/idml/text_engine.rb
CHANGED
|
@@ -5,8 +5,7 @@ module Idml
|
|
|
5
5
|
# geometry and produces positioned glyphs using font metrics from
|
|
6
6
|
# .ttf/.otf files. IDML-agnostic — works on any styled text.
|
|
7
7
|
module TextEngine
|
|
8
|
-
autoload :
|
|
9
|
-
autoload :FontResolver, "#{__dir__}/text_engine/font_resolver"
|
|
8
|
+
autoload :PdfrbFontMetrics, "#{__dir__}/text_engine/pdfrb_font_metrics"
|
|
10
9
|
autoload :Shaper, "#{__dir__}/text_engine/shaper"
|
|
11
10
|
autoload :ShapedGlyph, "#{__dir__}/text_engine/shaper"
|
|
12
11
|
autoload :LineBreaker, "#{__dir__}/text_engine/line_breaker"
|
data/lib/idml/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: idml
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -23,20 +23,6 @@ dependencies:
|
|
|
23
23
|
- - ">="
|
|
24
24
|
- !ruby/object:Gem::Version
|
|
25
25
|
version: '0'
|
|
26
|
-
- !ruby/object:Gem::Dependency
|
|
27
|
-
name: fontisan
|
|
28
|
-
requirement: !ruby/object:Gem::Requirement
|
|
29
|
-
requirements:
|
|
30
|
-
- - ">="
|
|
31
|
-
- !ruby/object:Gem::Version
|
|
32
|
-
version: '0'
|
|
33
|
-
type: :runtime
|
|
34
|
-
prerelease: false
|
|
35
|
-
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
-
requirements:
|
|
37
|
-
- - ">="
|
|
38
|
-
- !ruby/object:Gem::Version
|
|
39
|
-
version: '0'
|
|
40
26
|
- !ruby/object:Gem::Dependency
|
|
41
27
|
name: lutaml-model
|
|
42
28
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -218,6 +204,8 @@ files:
|
|
|
218
204
|
- TODO.pdf/75-pdf-metadata-from-xmp.md
|
|
219
205
|
- TODO.pdf/76-tagged-pdf-structure.md
|
|
220
206
|
- TODO.pdf/77-pdfa-xmp-output-intent.md
|
|
207
|
+
- TODO.pdf/78-hyperlinks.md
|
|
208
|
+
- TODO.pdf/79-bookmarks-outline.md
|
|
221
209
|
- TODO.pdf/README.md
|
|
222
210
|
- exe/idml
|
|
223
211
|
- idml.gemspec
|
|
@@ -389,8 +377,10 @@ files:
|
|
|
389
377
|
- lib/idml/render/font_reference_resolver.rb
|
|
390
378
|
- lib/idml/render/gradient_resolver.rb
|
|
391
379
|
- lib/idml/render/image.rb
|
|
380
|
+
- lib/idml/render/image_collector.rb
|
|
392
381
|
- lib/idml/render/layer_filter.rb
|
|
393
382
|
- lib/idml/render/page_item_renderer.rb
|
|
383
|
+
- lib/idml/render/pdfa_packet.rb
|
|
394
384
|
- lib/idml/render/pdfrb_writer.rb
|
|
395
385
|
- lib/idml/render/pipeline.rb
|
|
396
386
|
- lib/idml/render/placement.rb
|
|
@@ -405,13 +395,14 @@ files:
|
|
|
405
395
|
- lib/idml/render/spread_renderer.rb
|
|
406
396
|
- lib/idml/render/story_threader.rb
|
|
407
397
|
- lib/idml/render/stroke_style.rb
|
|
398
|
+
- lib/idml/render/structure_mapper.rb
|
|
399
|
+
- lib/idml/render/structure_tracker.rb
|
|
408
400
|
- lib/idml/render/style_resolver.rb
|
|
409
401
|
- lib/idml/text_engine.rb
|
|
410
402
|
- lib/idml/text_engine/cjk_layout.rb
|
|
411
|
-
- lib/idml/text_engine/font_metrics.rb
|
|
412
|
-
- lib/idml/text_engine/font_resolver.rb
|
|
413
403
|
- lib/idml/text_engine/justifier.rb
|
|
414
404
|
- lib/idml/text_engine/line_breaker.rb
|
|
405
|
+
- lib/idml/text_engine/pdfrb_font_metrics.rb
|
|
415
406
|
- lib/idml/text_engine/shaper.rb
|
|
416
407
|
- lib/idml/text_engine/vertical_layout.rb
|
|
417
408
|
- lib/idml/validation.rb
|