pdfrb 0.7.11 → 0.7.13
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/lib/pdfrb/composer.rb +25 -6
- data/lib/pdfrb/content/operators/inline_image.rb +6 -13
- data/lib/pdfrb/content/processor.rb +6 -0
- data/lib/pdfrb/layout/box_fitter.rb +35 -20
- data/lib/pdfrb/layout/frame.rb +52 -11
- data/lib/pdfrb/version.rb +1 -1
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ebb3cb6d645dd2e4f01dbfdcfc49e51ed0857281fc996a38ddb29afb18aef4e7
|
|
4
|
+
data.tar.gz: 9b359396f34164f661bebece73f26dd92fc9ea804c8edd802e53d80c6fccb334
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a3eb6c87f3e7b10fdb310669943b3f61cf206c10f3ad61a998673b83f94dc6aef3082a22ef8f56632dad2e75a858b39c7a969a0590779f8ae3b7320c6c78ff90
|
|
7
|
+
data.tar.gz: 9a1453e1556d9c968b3176aa207773e4083687441f7f8025941d82b4f34394a68fba1b3f7a386b4fb73743fb4770ca7c055276f28100b9686f40f6165eb69c76
|
data/lib/pdfrb/composer.rb
CHANGED
|
@@ -57,6 +57,8 @@ module Pdfrb
|
|
|
57
57
|
def new_page(style_name = nil)
|
|
58
58
|
@current_style_name = style_name || next_style_name
|
|
59
59
|
style_obj = @page_styles[@current_style_name]
|
|
60
|
+
# Reset the frame cursor for the new page.
|
|
61
|
+
reset_frame_cursor(style_obj)
|
|
60
62
|
page = @document.pages.add
|
|
61
63
|
page.value[:MediaBox] = [0, 0, style_obj.width, style_obj.height]
|
|
62
64
|
@current_page = page
|
|
@@ -64,6 +66,11 @@ module Pdfrb
|
|
|
64
66
|
page
|
|
65
67
|
end
|
|
66
68
|
|
|
69
|
+
def reset_frame_cursor(style_obj)
|
|
70
|
+
style_obj&.frame&.reset!
|
|
71
|
+
end
|
|
72
|
+
private :reset_frame_cursor
|
|
73
|
+
|
|
67
74
|
def x
|
|
68
75
|
@cursor_x || 50
|
|
69
76
|
end
|
|
@@ -143,17 +150,29 @@ module Pdfrb
|
|
|
143
150
|
|
|
144
151
|
style_obj = @page_styles[@current_style_name]
|
|
145
152
|
frame = style_obj.frame
|
|
146
|
-
cursor_y = frame.top
|
|
147
153
|
|
|
148
154
|
@pending_boxes.each do |box|
|
|
149
|
-
|
|
155
|
+
available_h = frame.available_height.positive? ? frame.available_height : (frame.top - frame.bottom)
|
|
156
|
+
unless box.fit?(frame.width, available_h)
|
|
150
157
|
new_page
|
|
151
|
-
|
|
152
|
-
|
|
158
|
+
style_obj = @page_styles[@current_style_name]
|
|
159
|
+
frame = style_obj.frame
|
|
160
|
+
available_h = frame.top - frame.bottom
|
|
161
|
+
box.fit?(frame.width, available_h)
|
|
153
162
|
end
|
|
163
|
+
|
|
164
|
+
position = frame.find_available_area(box.width || frame.width, box.height || available_h)
|
|
165
|
+
if position
|
|
166
|
+
draw_x, draw_y, = position
|
|
167
|
+
else
|
|
168
|
+
draw_x = frame.left
|
|
169
|
+
draw_y = frame.cursor_y - (box.height || 0)
|
|
170
|
+
end
|
|
171
|
+
|
|
154
172
|
canvas = @current_page.canvas
|
|
155
|
-
box.draw(canvas,
|
|
156
|
-
|
|
173
|
+
box.draw(canvas, draw_x, draw_y)
|
|
174
|
+
box_h = box.height || available_h
|
|
175
|
+
frame.remove_area(draw_x, draw_y, box.width || frame.width, box_h)
|
|
157
176
|
end
|
|
158
177
|
@pending_boxes = []
|
|
159
178
|
end
|
|
@@ -10,10 +10,9 @@ module Pdfrb
|
|
|
10
10
|
# needed, but the bytes live in the content stream so they
|
|
11
11
|
# can't be reused.
|
|
12
12
|
#
|
|
13
|
-
# The
|
|
14
|
-
#
|
|
15
|
-
#
|
|
16
|
-
# rather than via the operator dispatch table.
|
|
13
|
+
# The Parser detects BI ... ID ... EI and yields a single
|
|
14
|
+
# BeginInlineImage invocation with the parsed image Hash as
|
|
15
|
+
# the operand. The invoke hook calls Processor#inline_image.
|
|
17
16
|
class BeginInlineImage < Base
|
|
18
17
|
class << self
|
|
19
18
|
def name; "BI"; end
|
|
@@ -27,7 +26,9 @@ module Pdfrb
|
|
|
27
26
|
buf
|
|
28
27
|
end
|
|
29
28
|
|
|
30
|
-
def invoke(
|
|
29
|
+
def invoke(processor, image = nil)
|
|
30
|
+
processor.inline_image(image) if image
|
|
31
|
+
end
|
|
31
32
|
register
|
|
32
33
|
end
|
|
33
34
|
end
|
|
@@ -36,14 +37,6 @@ module Pdfrb
|
|
|
36
37
|
class << self
|
|
37
38
|
def name; "EI"; end
|
|
38
39
|
|
|
39
|
-
# The byte payload sits between ID and EI. The serializer
|
|
40
|
-
# for inline images writes the payload directly via
|
|
41
|
-
# Canvas#inline_image; this method is here so the registry
|
|
42
|
-
# knows about EI.
|
|
43
|
-
def serialize(_serializer, *_operands)
|
|
44
|
-
"EI\n"
|
|
45
|
-
end
|
|
46
|
-
|
|
47
40
|
def invoke(_processor, *_operands); end
|
|
48
41
|
register
|
|
49
42
|
end
|
|
@@ -76,6 +76,12 @@ module Pdfrb
|
|
|
76
76
|
def begin_marked_content(_tag, _properties = nil); end
|
|
77
77
|
def end_marked_content; end
|
|
78
78
|
def marked_content_point(_tag, _properties = nil); end
|
|
79
|
+
|
|
80
|
+
# Called when a BI/ID/EI inline image is encountered in the
|
|
81
|
+
# content stream. +image+ is a Hash with :header (the image
|
|
82
|
+
# dict keys, expanded from abbreviations) and :data (the raw
|
|
83
|
+
# byte payload). Subclasses override to render or extract.
|
|
84
|
+
def inline_image(_image); end
|
|
79
85
|
end
|
|
80
86
|
end
|
|
81
87
|
end
|
|
@@ -3,7 +3,9 @@
|
|
|
3
3
|
module Pdfrb
|
|
4
4
|
module Layout
|
|
5
5
|
# Fits an ordered list of boxes into a Frame, flowing to a new
|
|
6
|
-
# Frame (provided by a block) when one fills up.
|
|
6
|
+
# Frame (provided by a block) when one fills up. Delegates area
|
|
7
|
+
# tracking to the Frame (which now properly tracks removed areas
|
|
8
|
+
# and returns non-overlapping positions).
|
|
7
9
|
class BoxFitter
|
|
8
10
|
attr_reader :boxes, :frame, :result, :overflow
|
|
9
11
|
|
|
@@ -16,26 +18,8 @@ module Pdfrb
|
|
|
16
18
|
|
|
17
19
|
def fit
|
|
18
20
|
current_frame = @frame
|
|
19
|
-
cursor_y = current_frame.top
|
|
20
21
|
@boxes.each do |box|
|
|
21
|
-
|
|
22
|
-
position = current_frame.find_available_area(box.width || current_frame.width, box.height || (cursor_y - current_frame.bottom))
|
|
23
|
-
if position && box.fit?(position[2] || current_frame.width, position[3] || (cursor_y - current_frame.bottom))
|
|
24
|
-
@result << [box, position]
|
|
25
|
-
current_frame.remove_area(position[0], position[1] - box.height, box.width, box.height)
|
|
26
|
-
cursor_y = position[1] - box.height
|
|
27
|
-
break
|
|
28
|
-
else
|
|
29
|
-
next_frame = yield if block_given?
|
|
30
|
-
if next_frame
|
|
31
|
-
current_frame = next_frame
|
|
32
|
-
cursor_y = current_frame.top
|
|
33
|
-
else
|
|
34
|
-
@overflow << box
|
|
35
|
-
break
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
|
-
end
|
|
22
|
+
current_frame = fit_box(box, current_frame)
|
|
39
23
|
end
|
|
40
24
|
self
|
|
41
25
|
end
|
|
@@ -49,6 +33,37 @@ module Pdfrb
|
|
|
49
33
|
def fit_complete?
|
|
50
34
|
@overflow.empty?
|
|
51
35
|
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
def fit_box(box, frame)
|
|
40
|
+
loop do
|
|
41
|
+
return frame if place?(box, frame)
|
|
42
|
+
|
|
43
|
+
next_frame = yield if block_given?
|
|
44
|
+
unless next_frame
|
|
45
|
+
@overflow << box
|
|
46
|
+
return frame
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
frame = next_frame
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def place?(box, frame)
|
|
54
|
+
box_w = box.width || frame.width
|
|
55
|
+
box_h = box.height || frame.available_height
|
|
56
|
+
position = frame.find_available_area(box_w, box_h)
|
|
57
|
+
return false unless position
|
|
58
|
+
|
|
59
|
+
actual_w = position[2] || frame.width
|
|
60
|
+
actual_h = position[3] || frame.available_height
|
|
61
|
+
return false unless box.fit?(actual_w, actual_h)
|
|
62
|
+
|
|
63
|
+
@result << [box, position]
|
|
64
|
+
frame.remove_area(position[0], position[1], actual_w, box.height || actual_h)
|
|
65
|
+
true
|
|
66
|
+
end
|
|
52
67
|
end
|
|
53
68
|
end
|
|
54
69
|
end
|
data/lib/pdfrb/layout/frame.rb
CHANGED
|
@@ -9,8 +9,14 @@ module Pdfrb
|
|
|
9
9
|
# Simple case: the Frame is a plain rectangle. Advanced: the
|
|
10
10
|
# +shape+ polygon allows non-rectangular regions (L-shaped, with
|
|
11
11
|
# cutouts around images).
|
|
12
|
+
#
|
|
13
|
+
# Area tracking: +remove_area+ marks a rectangle as used. The
|
|
14
|
+
# Frame maintains a +cursor_y+ that tracks the lowest free
|
|
15
|
+
# vertical position. +find_available_area+ returns the next
|
|
16
|
+
# position at or above the cursor where the requested
|
|
17
|
+
# (width × height) fits without overlapping any removed area.
|
|
12
18
|
class Frame
|
|
13
|
-
attr_reader :left, :bottom, :width, :height, :shape
|
|
19
|
+
attr_reader :left, :bottom, :width, :height, :shape, :cursor_y
|
|
14
20
|
|
|
15
21
|
def initialize(left:, bottom:, width:, height:, shape: nil)
|
|
16
22
|
@left = left.to_f
|
|
@@ -18,6 +24,8 @@ module Pdfrb
|
|
|
18
24
|
@width = width.to_f
|
|
19
25
|
@height = height.to_f
|
|
20
26
|
@shape = shape || default_shape
|
|
27
|
+
@cursor_y = top
|
|
28
|
+
@removed_areas = []
|
|
21
29
|
end
|
|
22
30
|
|
|
23
31
|
def right
|
|
@@ -28,37 +36,70 @@ module Pdfrb
|
|
|
28
36
|
@bottom + @height
|
|
29
37
|
end
|
|
30
38
|
|
|
31
|
-
# Locate the next free area of (width × height) within this
|
|
32
|
-
#
|
|
33
|
-
#
|
|
34
|
-
#
|
|
39
|
+
# Locate the next free area of (width × height) within this
|
|
40
|
+
# Frame. Scans from the cursor_y downward, checking each
|
|
41
|
+
# candidate y against the removed-areas list. Returns
|
|
42
|
+
# [x, y, w, h] or nil if nothing fits.
|
|
35
43
|
def find_available_area(width, height)
|
|
36
44
|
w = width.to_f
|
|
37
45
|
h = height.to_f
|
|
38
46
|
return nil if w > @width || h > @height
|
|
47
|
+
return nil if h > available_height
|
|
48
|
+
|
|
49
|
+
y = @cursor_y - h
|
|
50
|
+
while y >= @bottom
|
|
51
|
+
if area_free?(@left, y, w, h)
|
|
52
|
+
@cursor_y = y
|
|
53
|
+
return [@left, y, w, h]
|
|
54
|
+
end
|
|
39
55
|
|
|
40
|
-
|
|
56
|
+
y -= 1
|
|
57
|
+
end
|
|
58
|
+
nil
|
|
41
59
|
end
|
|
42
60
|
|
|
43
61
|
# Mark the rectangle at (x, y) of (w × h) as filled. Subsequent
|
|
44
|
-
# +find_available_area+ calls avoid this region.
|
|
62
|
+
# +find_available_area+ calls avoid this region. Moves the
|
|
63
|
+
# cursor down when the removed area is at the current cursor.
|
|
45
64
|
def remove_area(x, y, w, h)
|
|
46
65
|
return if x.nil? || y.nil?
|
|
47
66
|
|
|
48
|
-
@
|
|
49
|
-
|
|
67
|
+
@removed_areas << [x.to_f, y.to_f, w.to_f, h.to_f]
|
|
68
|
+
# If the removed area touches the cursor, advance it.
|
|
69
|
+
if (y + h) >= @cursor_y - 1
|
|
70
|
+
@cursor_y = [@cursor_y, y].min
|
|
71
|
+
end
|
|
50
72
|
end
|
|
51
73
|
|
|
52
74
|
def full?
|
|
53
|
-
@
|
|
75
|
+
@cursor_y <= @bottom
|
|
54
76
|
end
|
|
55
77
|
|
|
56
78
|
def empty?
|
|
57
|
-
|
|
79
|
+
@removed_areas.empty?
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def available_height
|
|
83
|
+
@cursor_y - @bottom
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Reset the cursor and removed-areas list. Used by the
|
|
87
|
+
# Composer when starting a new page with the same Frame.
|
|
88
|
+
def reset!
|
|
89
|
+
@cursor_y = top
|
|
90
|
+
@removed_areas = []
|
|
58
91
|
end
|
|
59
92
|
|
|
60
93
|
private
|
|
61
94
|
|
|
95
|
+
def area_free?(x, y, w, h)
|
|
96
|
+
@removed_areas.none? { |rx, ry, rw, rh| rectangles_overlap?(x, y, w, h, rx, ry, rw, rh) }
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def rectangles_overlap?(x1, y1, w1, h1, x2, y2, w2, h2)
|
|
100
|
+
x1 < x2 + w2 && x1 + w1 > x2 && y1 < y2 + h2 && y1 + h1 > y2
|
|
101
|
+
end
|
|
102
|
+
|
|
62
103
|
def default_shape
|
|
63
104
|
[[@left, @bottom], [right, @bottom], [right, top], [@left, top]]
|
|
64
105
|
end
|
data/lib/pdfrb/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: pdfrb
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.7.
|
|
4
|
+
version: 0.7.13
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ribose Inc.
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: exe
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-08-
|
|
11
|
+
date: 2026-08-14 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: thor
|