idml 0.8.5 → 0.8.7

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ac8e5f6dd1f360428509a0ae78c7c537b80cbd1d41fa5e85f5e1fe61f0cac48e
4
- data.tar.gz: 2dcf34fd3fe19ef2978783e6857bd724872b741b8aab9b5418d77a10431b8aad
3
+ metadata.gz: ea725628da813f202ff5f6c8c9659e1de20b24d8eb9d92c6cd8cc43aad12948c
4
+ data.tar.gz: 3ae66c7b8c22ba9660da4a8bae4a8f2314cb13e90aa72f0c673350415ab77cfc
5
5
  SHA512:
6
- metadata.gz: 30deff6b7352132cf5c019bde7423294232fafbe7b25898e7188d754f721ba3d6a498f6e41f31ef86afafe4f4e768dfb459fc272eda6ac288df1ab033cf88cc2
7
- data.tar.gz: ba95fecbb82c8546c3ec4e31c3b3fdebfe1237fd8b7b5ec8e0a53c54d612dff72d9f1e3c89c6f88b8b7d5309f579a94a5e60132a50b37739914f21595bcf4b8f
6
+ metadata.gz: e6ec5c19fb106644c373ee8a3cd21526cfaff1ce88c7d56d7c65b6376fe508ba984203d2d7d7249d426fc9486d21f5d7dbc1f758c026dd6f60c923631e31a4d8
7
+ data.tar.gz: 82cd84ff102aafce7c0766e1bb3aea5393a20f4d959753273ad163f15a9b83ab22a323c8b369548b45a3cc6d3e0ec5857eac1663ccdb601e9adb093c7067121e
@@ -1,6 +1,20 @@
1
1
  # TODO PDF 114: Text wrap around shapes (TextWrapPreference)
2
2
 
3
- ## Status: OPEN gap identified in 2026-08-10 audit
3
+ ## Status: DONE for BoundingBox mode `Render::TextWrapResolver` computes
4
+ wrap contours from page items declaring TextWrapPreference with
5
+ TextWrapMode != "None". SpreadRenderer builds the resolver and passes
6
+ it via RenderContext. TextFrameRenderer's `text_wrap_overlap` queries
7
+ overlap at each run's y position and reduces the wrap width — text
8
+ flows around the shape instead of running over it.
9
+
10
+ Shape page items (Rectangle, Oval, Polygon, GraphicLine, Path) all
11
+ carry the TextWrapPreference child element. Non-shape items (Page,
12
+ Link) are filtered by WRAPPABLE_TYPES to avoid NoMethodError.
13
+
14
+ Per-run approximation: all lines in a run get the same reduced width.
15
+ True per-line adjustment (when a run spans the contour boundary)
16
+ requires line-count-aware layout. Shape mode (contour following) and
17
+ Inverse mode remain unsupported.
4
18
 
5
19
  ## Problem
6
20
 
@@ -18,6 +18,7 @@ module Idml
18
18
  attribute :item_layer, :string
19
19
  attribute :properties, Idml::Elements::Properties, collection: true
20
20
  attribute :transparency_setting, Idml::Elements::TransparencySetting
21
+ attribute :text_wrap_preference, Idml::Elements::TextWrapPreference
21
22
 
22
23
  xml do
23
24
  root "GraphicLine"
@@ -34,6 +35,7 @@ module Idml
34
35
  map_attribute "ItemLayer", to: :item_layer
35
36
  map_element "Properties", to: :properties
36
37
  map_element "TransparencySetting", to: :transparency_setting
38
+ map_element "TextWrapPreference", to: :text_wrap_preference
37
39
  end
38
40
 
39
41
  def geometric_bounds
@@ -29,6 +29,7 @@ module Idml
29
29
  attribute :item_layer, :string
30
30
  attribute :properties, Idml::Elements::Properties, collection: true
31
31
  attribute :transparency_setting, Idml::Elements::TransparencySetting
32
+ attribute :text_wrap_preference, Idml::Elements::TextWrapPreference
32
33
  attribute :image, Idml::Elements::Image, collection: true
33
34
 
34
35
  xml do
@@ -50,6 +51,7 @@ module Idml
50
51
  map_attribute "ItemLayer", to: :item_layer
51
52
  map_element "Properties", to: :properties
52
53
  map_element "TransparencySetting", to: :transparency_setting
54
+ map_element "TextWrapPreference", to: :text_wrap_preference
53
55
  map_element "Image", to: :image
54
56
  end
55
57
 
@@ -25,6 +25,7 @@ module Idml
25
25
  attribute :item_layer, :string
26
26
  attribute :properties, Idml::Elements::Properties, collection: true
27
27
  attribute :transparency_setting, Idml::Elements::TransparencySetting
28
+ attribute :text_wrap_preference, Idml::Elements::TextWrapPreference
28
29
 
29
30
  xml do
30
31
  root "Path"
@@ -41,6 +42,7 @@ module Idml
41
42
  map_attribute "ItemLayer", to: :item_layer
42
43
  map_element "Properties", to: :properties
43
44
  map_element "TransparencySetting", to: :transparency_setting
45
+ map_element "TextWrapPreference", to: :text_wrap_preference
44
46
  end
45
47
 
46
48
  def geometric_bounds
@@ -20,6 +20,7 @@ module Idml
20
20
  attribute :item_layer, :string
21
21
  attribute :properties, Idml::Elements::Properties, collection: true
22
22
  attribute :transparency_setting, Idml::Elements::TransparencySetting
23
+ attribute :text_wrap_preference, Idml::Elements::TextWrapPreference
23
24
  attribute :image, Idml::Elements::Image, collection: true
24
25
 
25
26
  xml do
@@ -39,6 +40,7 @@ module Idml
39
40
  map_attribute "ItemLayer", to: :item_layer
40
41
  map_element "Properties", to: :properties
41
42
  map_element "TransparencySetting", to: :transparency_setting
43
+ map_element "TextWrapPreference", to: :text_wrap_preference
42
44
  map_element "Image", to: :image
43
45
  end
44
46
 
@@ -24,6 +24,7 @@ module Idml
24
24
  attribute :item_layer, :string
25
25
  attribute :properties, Idml::Elements::Properties, collection: true
26
26
  attribute :transparency_setting, Idml::Elements::TransparencySetting
27
+ attribute :text_wrap_preference, Idml::Elements::TextWrapPreference
27
28
  attribute :image, Idml::Elements::Image, collection: true
28
29
 
29
30
  xml do
@@ -46,6 +47,7 @@ module Idml
46
47
  map_attribute "ItemLayer", to: :item_layer
47
48
  map_element "Properties", to: :properties
48
49
  map_element "TransparencySetting", to: :transparency_setting
50
+ map_element "TextWrapPreference", to: :text_wrap_preference
49
51
  map_element "Image", to: :image
50
52
  end
51
53
 
@@ -23,6 +23,7 @@ module Idml
23
23
  :chain_controller,
24
24
  :condition_filter,
25
25
  :style_lookup,
26
+ :text_wrap_resolver,
26
27
  keyword_init: true,
27
28
  )
28
29
  end
@@ -436,6 +436,8 @@ module Idml
436
436
  rendered_count)
437
437
  run_wrap = drop_cap_wrap_width(wrap_width, dc_width,
438
438
  dc_lines, rendered_count)
439
+ run_wrap -= text_wrap_overlap(context, layout_frame, cursor_y,
440
+ run)
439
441
  positioned, next_y = layout_run(
440
442
  canvas, run, paragraph, context, layout_frame, font,
441
443
  run_wrap, cursor_y, space_before,
@@ -450,6 +452,27 @@ module Idml
450
452
  end
451
453
  private_class_method :iterate_paragraph_runs
452
454
 
455
+ # Computes the text-wrap overlap for a run at its current y
456
+ # position. Uses the run's point_size as the line height
457
+ # approximation. Returns 0.0 when no resolver is wired or no
458
+ # contours overlap. Per-run approximation: all lines in the
459
+ # run get the same reduced width (correct when the run is
460
+ # fully inside or outside the contour; approximate when it
461
+ # spans the boundary).
462
+ def self.text_wrap_overlap(context, layout_frame, cursor_y, run)
463
+ resolver = context.text_wrap_resolver
464
+ return 0.0 unless resolver
465
+
466
+ size = run.point_size || DEFAULT_SIZE
467
+ frame_x = layout_frame.x + (layout_frame.inset_left || 0)
468
+ frame_right = layout_frame.x + layout_frame.width -
469
+ (layout_frame.inset_right || 0)
470
+ overlap = resolver.overlap_width(cursor_y, size,
471
+ frame_x, frame_right)
472
+ [overlap, 0.0].max
473
+ end
474
+ private_class_method :text_wrap_overlap
475
+
453
476
  # Strips drop cap characters from the first run so they don't
454
477
  # render twice (once as the enlarged drop cap, once as normal
455
478
  # text). Returns a new run list with the first run's text
@@ -39,6 +39,7 @@ module Idml
39
39
  chain_controller: StoryChainController.new,
40
40
  condition_filter: @condition_filter,
41
41
  style_lookup: @style_lookup,
42
+ text_wrap_resolver: build_text_wrap_resolver(spread, page_height),
42
43
  }
43
44
  images_by_parent = group_images_by_parent(image_refs)
44
45
 
@@ -144,6 +145,10 @@ module Idml
144
145
 
145
146
  ColorResolver.new(@package.graphic)
146
147
  end
148
+
149
+ def build_text_wrap_resolver(spread, page_height)
150
+ TextWrapResolver.build(spread, page_height: page_height)
151
+ end
147
152
  end
148
153
  end
149
154
  end
@@ -0,0 +1,129 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Idml
4
+ module Render
5
+ # Resolves text-wrap contours from page items that declare
6
+ # TextWrapPreference. For BoundingBox mode (the common case),
7
+ # the contour is the item's geometric bounds expanded by the
8
+ # wrap offsets. TextFrameRenderer queries this resolver per
9
+ # text line to compute the effective wrap width — lines that
10
+ # overlap a contour get their width reduced by the overlap.
11
+ #
12
+ # Supported: BoundingBox mode (rectangle intersection).
13
+ # Unsupported: Shape mode (contour following), Inverse mode.
14
+ class TextWrapResolver
15
+ # A wrap contour: a PDF-coordinate rectangle the text avoids.
16
+ Contour = Struct.new(
17
+ :x, :y, :width, :height,
18
+ keyword_init: true
19
+ )
20
+
21
+ OFFSET_ATTRS = %i[
22
+ offset_top offset_left offset_bottom offset_right
23
+ ].freeze
24
+
25
+ def self.build(spread, page_height: 792)
26
+ contours = []
27
+ spread.each_page_item do |item|
28
+ contour = contour_for(item, page_height)
29
+ contours << contour if contour
30
+ end
31
+ new(contours)
32
+ end
33
+
34
+ def initialize(contours)
35
+ @contours = contours
36
+ end
37
+
38
+ attr_reader :contours
39
+
40
+ # Returns the total horizontal overlap of all contours with a
41
+ # text line at the given y range within the given x range.
42
+ # Sum, not max — multiple overlapping contours compound.
43
+ # Returns 0.0 when there's no overlap.
44
+ def overlap_width(line_y, line_height, frame_x, frame_right)
45
+ @contours.sum do |contour|
46
+ overlap = line_overlap(contour, line_y, line_height,
47
+ frame_x, frame_right)
48
+ overlap[:width]
49
+ end
50
+ end
51
+
52
+ def self.contour_for(item, page_height)
53
+ pref = wrap_preference(item)
54
+ return nil unless pref
55
+ return nil unless bounding_box_mode?(pref)
56
+
57
+ bounds = item.geometric_bounds
58
+ return nil unless bounds
59
+
60
+ rect = Geometry.placement_rect(bounds, item.item_transform,
61
+ page_height)
62
+ Contour.new(
63
+ x: rect[:x], y: rect[:y],
64
+ width: rect[:width], height: rect[:height]
65
+ )
66
+ end
67
+ private_class_method :contour_for
68
+
69
+ # Page item types that can declare TextWrapPreference per the
70
+ # Spread schema. Used to guard the attribute read — querying
71
+ # non-shape items (Page, Link) would raise NoMethodError.
72
+ WRAPPABLE_TYPES = [
73
+ Idml::Elements::Rectangle,
74
+ Idml::Elements::Oval,
75
+ Idml::Elements::Polygon,
76
+ Idml::Elements::GraphicLine,
77
+ Idml::Elements::Path,
78
+ Idml::Elements::Group,
79
+ ].freeze
80
+
81
+ def self.wrap_preference(item)
82
+ return nil unless wrappable?(item)
83
+
84
+ pref = item.text_wrap_preference
85
+ return nil if pref.nil? ||
86
+ pref.text_wrap_mode.nil? ||
87
+ pref.text_wrap_mode == "None"
88
+
89
+ pref
90
+ end
91
+ private_class_method :wrap_preference
92
+
93
+ def self.wrappable?(item)
94
+ WRAPPABLE_TYPES.any? { |type| item.is_a?(type) }
95
+ end
96
+ private_class_method :wrappable?
97
+
98
+ def self.bounding_box_mode?(pref)
99
+ pref.text_wrap_mode == "BoundingBox"
100
+ end
101
+ private_class_method :bounding_box_mode?
102
+
103
+ def line_overlap(contour, line_y, line_height, frame_x, frame_right)
104
+ return { width: 0.0 } unless y_overlap?(contour, line_y, line_height)
105
+ return { width: 0.0 } unless x_overlap?(contour, frame_x, frame_right)
106
+
107
+ { width: x_overlap_width(contour, frame_x, frame_right) }
108
+ end
109
+
110
+ def y_overlap?(contour, line_y, line_height)
111
+ line_top = line_y + line_height
112
+ line_bottom = line_y
113
+ contour_top = contour.y + contour.height
114
+ contour_bottom = contour.y
115
+ line_top > contour_bottom && line_bottom < contour_top
116
+ end
117
+
118
+ def x_overlap?(contour, frame_x, frame_right)
119
+ contour.x < frame_right && contour.x + contour.width > frame_x
120
+ end
121
+
122
+ def x_overlap_width(contour, frame_x, frame_right)
123
+ left = [contour.x, frame_x].max
124
+ right = [contour.x + contour.width, frame_right].min
125
+ [right - left, 0].max
126
+ end
127
+ end
128
+ end
129
+ end
data/lib/idml/render.rb CHANGED
@@ -21,6 +21,7 @@ module Idml
21
21
  autoload :StoryChainController, "#{__dir__}/render/story_chain_controller"
22
22
  autoload :LayerFilter, "#{__dir__}/render/layer_filter"
23
23
  autoload :ConditionFilter, "#{__dir__}/render/condition_filter"
24
+ autoload :TextWrapResolver, "#{__dir__}/render/text_wrap_resolver"
24
25
  autoload :Placement, "#{__dir__}/render/placement"
25
26
  autoload :ImageCollector, "#{__dir__}/render/image_collector"
26
27
  autoload :StructureTracker, "#{__dir__}/render/structure_tracker"
data/lib/idml/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Idml
4
- VERSION = "0.8.5"
4
+ VERSION = "0.8.7"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: idml
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.5
4
+ version: 0.8.7
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-14 00:00:00.000000000 Z
11
+ date: 2026-08-16 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bigdecimal
@@ -475,6 +475,7 @@ files:
475
475
  - lib/idml/render/structure_tracker.rb
476
476
  - lib/idml/render/style_lookup.rb
477
477
  - lib/idml/render/style_resolver.rb
478
+ - lib/idml/render/text_wrap_resolver.rb
478
479
  - lib/idml/text_engine.rb
479
480
  - lib/idml/text_engine/cjk_layout.rb
480
481
  - lib/idml/text_engine/justifier.rb