idml 0.2.8 → 0.3.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.lock +4 -4
- data/TODO.pdf/75-pdf-metadata-from-xmp.md +52 -90
- 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/lib/idml/parts/xmp.rb +305 -0
- data/lib/idml/parts.rb +4 -0
- 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 +69 -90
- data/lib/idml/render/render_context.rb +2 -0
- data/lib/idml/render/spread_renderer.rb +6 -2
- 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/version.rb +1 -1
- metadata +8 -1
data/lib/idml/render/pipeline.rb
CHANGED
|
@@ -21,18 +21,24 @@ 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_name = register_font(writer)
|
|
33
|
+
page_index = -1
|
|
30
34
|
|
|
31
35
|
@package.spreads.each do |spread|
|
|
32
|
-
render_spread_pages(writer, spread, base_dir,
|
|
33
|
-
|
|
36
|
+
page_index = render_spread_pages(writer, spread, base_dir,
|
|
37
|
+
layer_filter, font_ref_resolver,
|
|
38
|
+
font_name, structure, page_index)
|
|
34
39
|
end
|
|
35
40
|
|
|
41
|
+
structure.flush(writer)
|
|
36
42
|
writer.build_structure if @tagged
|
|
37
43
|
writer.subset_fonts! if @subset_fonts
|
|
38
44
|
writer.write(@output_path)
|
|
@@ -41,113 +47,43 @@ module Idml
|
|
|
41
47
|
|
|
42
48
|
private
|
|
43
49
|
|
|
50
|
+
def pdfa_requested?
|
|
51
|
+
@compliance&.to_s&.start_with?("pdfa")
|
|
52
|
+
end
|
|
53
|
+
|
|
44
54
|
def render_spread_pages(writer, spread, base_dir, layer_filter,
|
|
45
|
-
font_ref_resolver, font_name
|
|
55
|
+
font_ref_resolver, font_name, structure,
|
|
56
|
+
page_offset)
|
|
46
57
|
pages = spread.spread.flat_map(&:page)
|
|
47
|
-
image_refs =
|
|
48
|
-
|
|
58
|
+
image_refs = ImageCollector.new(writer: writer, base_dir: base_dir,
|
|
59
|
+
page_height: DEFAULT_HEIGHT).collect(spread)
|
|
60
|
+
renderer = build_renderer(layer_filter, font_ref_resolver, font_name,
|
|
61
|
+
structure: structure)
|
|
62
|
+
current = page_offset
|
|
49
63
|
|
|
50
64
|
pages.each do |page|
|
|
65
|
+
current += 1
|
|
51
66
|
dims = page_dimensions_for(page)
|
|
52
67
|
canvas = writer.add_page(width: dims[:width], height: dims[:height])
|
|
53
68
|
renderer.render(canvas, spread, page_width: dims[:width],
|
|
54
69
|
page_height: dims[:height],
|
|
55
|
-
image_refs: image_refs
|
|
70
|
+
image_refs: image_refs,
|
|
71
|
+
page_index: current)
|
|
56
72
|
end
|
|
73
|
+
current
|
|
57
74
|
end
|
|
58
75
|
|
|
59
|
-
def build_renderer(layer_filter, font_ref_resolver, font_name)
|
|
76
|
+
def build_renderer(layer_filter, font_ref_resolver, font_name, structure:)
|
|
60
77
|
SpreadRenderer.new(
|
|
61
78
|
font_resolver: @font_resolver,
|
|
62
79
|
font_ps_name: font_name,
|
|
63
80
|
package: @package,
|
|
64
81
|
layer_filter: layer_filter,
|
|
65
82
|
font_ref_resolver: font_ref_resolver,
|
|
83
|
+
structure: structure,
|
|
66
84
|
)
|
|
67
85
|
end
|
|
68
86
|
|
|
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
87
|
def page_dimensions_for(page)
|
|
152
88
|
{ width: page.width || DEFAULT_WIDTH,
|
|
153
89
|
height: page.height || DEFAULT_HEIGHT }
|
|
@@ -190,6 +126,46 @@ module Idml
|
|
|
190
126
|
nil
|
|
191
127
|
end
|
|
192
128
|
|
|
129
|
+
def combined_metadata
|
|
130
|
+
defaults = default_metadata
|
|
131
|
+
xmp_metadata.each do |key, value|
|
|
132
|
+
next if value.nil? || value.empty?
|
|
133
|
+
|
|
134
|
+
defaults[key] = value
|
|
135
|
+
end
|
|
136
|
+
defaults
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def xmp_metadata
|
|
140
|
+
return {} unless @package.has_part?(XMP_PATH)
|
|
141
|
+
|
|
142
|
+
xml = @package.read_part(XMP_PATH)
|
|
143
|
+
meta = Parts::XmpMeta.from_xml(xml)
|
|
144
|
+
rdf = meta.rdf
|
|
145
|
+
return {} unless rdf
|
|
146
|
+
|
|
147
|
+
{
|
|
148
|
+
Title: rdf.title,
|
|
149
|
+
Author: rdf.author,
|
|
150
|
+
Subject: rdf.description,
|
|
151
|
+
Keywords: rdf.keywords,
|
|
152
|
+
Creator: rdf.creator_tool,
|
|
153
|
+
CreationDate: pdf_date_string(rdf.create_date),
|
|
154
|
+
ModDate: pdf_date_string(rdf.modify_date),
|
|
155
|
+
}
|
|
156
|
+
rescue StandardError
|
|
157
|
+
{}
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def pdf_date_string(iso8601)
|
|
161
|
+
return nil unless iso8601
|
|
162
|
+
|
|
163
|
+
time = Time.iso8601(iso8601)
|
|
164
|
+
pdf_date(time)
|
|
165
|
+
rescue ArgumentError
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
193
169
|
def default_metadata
|
|
194
170
|
{
|
|
195
171
|
Producer: "idml gem v#{Idml::VERSION}",
|
|
@@ -200,6 +176,9 @@ module Idml
|
|
|
200
176
|
def pdf_date(time)
|
|
201
177
|
time.strftime("D:%Y%m%d%H%M%S+00'00'")
|
|
202
178
|
end
|
|
179
|
+
|
|
180
|
+
XMP_PATH = "META-INF/metadata.xml"
|
|
181
|
+
private_constant :XMP_PATH
|
|
203
182
|
end
|
|
204
183
|
end
|
|
205
184
|
end
|
|
@@ -6,15 +6,17 @@ module Idml
|
|
|
6
6
|
class SpreadRenderer
|
|
7
7
|
def initialize(font_resolver: nil, font_ps_name: Render::DEFAULT_FONT,
|
|
8
8
|
package: nil, layer_filter: LayerFilter::EXCLUDE_NONE,
|
|
9
|
-
font_ref_resolver: nil)
|
|
9
|
+
font_ref_resolver: nil, structure: nil)
|
|
10
10
|
@font_resolver = font_resolver
|
|
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_resolver: @font_resolver,
|
|
@@ -24,6 +26,8 @@ module Idml
|
|
|
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"
|
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.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose
|
|
@@ -218,6 +218,8 @@ files:
|
|
|
218
218
|
- TODO.pdf/75-pdf-metadata-from-xmp.md
|
|
219
219
|
- TODO.pdf/76-tagged-pdf-structure.md
|
|
220
220
|
- TODO.pdf/77-pdfa-xmp-output-intent.md
|
|
221
|
+
- TODO.pdf/78-hyperlinks.md
|
|
222
|
+
- TODO.pdf/79-bookmarks-outline.md
|
|
221
223
|
- TODO.pdf/README.md
|
|
222
224
|
- exe/idml
|
|
223
225
|
- idml.gemspec
|
|
@@ -381,6 +383,7 @@ files:
|
|
|
381
383
|
- lib/idml/parts/style.rb
|
|
382
384
|
- lib/idml/parts/style_mapping.rb
|
|
383
385
|
- lib/idml/parts/tags.rb
|
|
386
|
+
- lib/idml/parts/xmp.rb
|
|
384
387
|
- lib/idml/render.rb
|
|
385
388
|
- lib/idml/render/blending.rb
|
|
386
389
|
- lib/idml/render/color_helper.rb
|
|
@@ -388,8 +391,10 @@ files:
|
|
|
388
391
|
- lib/idml/render/font_reference_resolver.rb
|
|
389
392
|
- lib/idml/render/gradient_resolver.rb
|
|
390
393
|
- lib/idml/render/image.rb
|
|
394
|
+
- lib/idml/render/image_collector.rb
|
|
391
395
|
- lib/idml/render/layer_filter.rb
|
|
392
396
|
- lib/idml/render/page_item_renderer.rb
|
|
397
|
+
- lib/idml/render/pdfa_packet.rb
|
|
393
398
|
- lib/idml/render/pdfrb_writer.rb
|
|
394
399
|
- lib/idml/render/pipeline.rb
|
|
395
400
|
- lib/idml/render/placement.rb
|
|
@@ -404,6 +409,8 @@ files:
|
|
|
404
409
|
- lib/idml/render/spread_renderer.rb
|
|
405
410
|
- lib/idml/render/story_threader.rb
|
|
406
411
|
- lib/idml/render/stroke_style.rb
|
|
412
|
+
- lib/idml/render/structure_mapper.rb
|
|
413
|
+
- lib/idml/render/structure_tracker.rb
|
|
407
414
|
- lib/idml/render/style_resolver.rb
|
|
408
415
|
- lib/idml/text_engine.rb
|
|
409
416
|
- lib/idml/text_engine/cjk_layout.rb
|