silk_layout 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/CHANGELOG.md +36 -1
- data/README.md +74 -29
- data/lib/silk_layout/css/cascade.rb +5 -2
- data/lib/silk_layout/css/color.rb +415 -0
- data/lib/silk_layout/css/computed_style.rb +43 -11
- data/lib/silk_layout/css/page_rule.rb +104 -0
- data/lib/silk_layout/css/parser.rb +122 -27
- data/lib/silk_layout/css/properties.rb +301 -3
- data/lib/silk_layout/css/selector.rb +246 -30
- data/lib/silk_layout/css/values.rb +169 -0
- data/lib/silk_layout/html/node.rb +1 -0
- data/lib/silk_layout/html/parser.rb +17 -3
- data/lib/silk_layout/layout/block_layout.rb +131 -12
- data/lib/silk_layout/layout/box.rb +39 -1
- data/lib/silk_layout/layout/context.rb +7 -2
- data/lib/silk_layout/layout/engine.rb +12 -2
- data/lib/silk_layout/layout/flex_layout.rb +1 -0
- data/lib/silk_layout/layout/formatting_builder.rb +73 -19
- data/lib/silk_layout/layout/inline_formatter.rb +88 -10
- data/lib/silk_layout/layout/root.rb +4 -5
- data/lib/silk_layout/render/painter.rb +17 -23
- data/lib/silk_layout/render/pdf_renderer.rb +39 -5
- data/lib/silk_layout/resource/image.rb +151 -0
- data/lib/silk_layout/version.rb +1 -1
- data/lib/silk_layout.rb +54 -4
- metadata +5 -1
|
@@ -4,6 +4,9 @@ module SilkLayout
|
|
|
4
4
|
module Layout
|
|
5
5
|
class BlockLayout
|
|
6
6
|
def self.layout(box, context, cursor_y = 0, parent_x = 0, containing_width = nil)
|
|
7
|
+
available_width = containing_width || context.width
|
|
8
|
+
resolve_spacing(box, available_width)
|
|
9
|
+
|
|
7
10
|
return FlexLayout.layout(box, context, cursor_y, parent_x, containing_width) if box.is_a?(FlexBox)
|
|
8
11
|
|
|
9
12
|
box.x = parent_x + box.margin[:left]
|
|
@@ -15,17 +18,7 @@ module SilkLayout
|
|
|
15
18
|
content_y =
|
|
16
19
|
box.y + box.border[:top] + box.padding[:top]
|
|
17
20
|
|
|
18
|
-
|
|
19
|
-
if box.explicit_width
|
|
20
|
-
content_width = box.width
|
|
21
|
-
else
|
|
22
|
-
content_width =
|
|
23
|
-
available_width -
|
|
24
|
-
box.margin[:left] - box.margin[:right] -
|
|
25
|
-
box.border[:left] - box.border[:right] -
|
|
26
|
-
box.padding[:left] - box.padding[:right]
|
|
27
|
-
content_width = 0 if content_width < 0
|
|
28
|
-
end
|
|
21
|
+
content_width = resolved_content_width(box, available_width)
|
|
29
22
|
|
|
30
23
|
current_y = content_y
|
|
31
24
|
new_children = []
|
|
@@ -71,7 +64,9 @@ module SilkLayout
|
|
|
71
64
|
box.children = new_children
|
|
72
65
|
|
|
73
66
|
content_height = current_y - content_y
|
|
74
|
-
|
|
67
|
+
if box.explicit_height
|
|
68
|
+
content_height = [content_height, content_height_from_css_height(box, declared_height(box))].max
|
|
69
|
+
end
|
|
75
70
|
|
|
76
71
|
max_child_width =
|
|
77
72
|
box.children.map(&:width).max || 0
|
|
@@ -88,6 +83,130 @@ module SilkLayout
|
|
|
88
83
|
box.padding[:top] + box.padding[:bottom] +
|
|
89
84
|
box.border[:top] + box.border[:bottom]
|
|
90
85
|
end
|
|
86
|
+
|
|
87
|
+
def self.resolve_spacing(box, available_width)
|
|
88
|
+
box.margin = resolved_edges(box, "margin", available_width, box.margin)
|
|
89
|
+
box.padding = resolved_edges(box, "padding", available_width, box.padding)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.resolved_edges(box, property, available_width, fallback)
|
|
93
|
+
style = box.node&.computed_style
|
|
94
|
+
values = CSS::Values.expanded_edges(style&.[](property))
|
|
95
|
+
|
|
96
|
+
{
|
|
97
|
+
top: resolved_edge(style, property, :top, values, available_width, fallback[:top]),
|
|
98
|
+
right: resolved_edge(style, property, :right, values, available_width, fallback[:right]),
|
|
99
|
+
bottom: resolved_edge(style, property, :bottom, values, available_width, fallback[:bottom]),
|
|
100
|
+
left: resolved_edge(style, property, :left, values, available_width, fallback[:left])
|
|
101
|
+
}
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def self.resolved_edge(style, property, side, values, available_width, fallback)
|
|
105
|
+
raw = style&.[]("#{property}-#{side}") || values[side]
|
|
106
|
+
return fallback unless raw
|
|
107
|
+
|
|
108
|
+
CSS::Values.resolve_length(raw, reference: available_width, default: fallback)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def self.resolved_content_width(box, available_width)
|
|
112
|
+
content_width =
|
|
113
|
+
if box.explicit_width
|
|
114
|
+
content_width_from_css_width(box, declared_width(box, available_width))
|
|
115
|
+
else
|
|
116
|
+
auto_content_width(box, available_width)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
[content_width, 0].max
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def self.declared_width(box, available_width)
|
|
123
|
+
raw = style_value(box, "width")
|
|
124
|
+
width =
|
|
125
|
+
if raw && raw != "auto" && !preserve_assigned_width?(box, raw, available_width)
|
|
126
|
+
CSS::Values.resolve_length(raw, reference: available_width, default: box.width)
|
|
127
|
+
else
|
|
128
|
+
box.width
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
constrain_css_width(box, width, available_width)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.auto_content_width(box, available_width)
|
|
135
|
+
content_width =
|
|
136
|
+
available_width -
|
|
137
|
+
box.margin[:left] - box.margin[:right] -
|
|
138
|
+
box.border[:left] - box.border[:right] -
|
|
139
|
+
box.padding[:left] - box.padding[:right]
|
|
140
|
+
|
|
141
|
+
constrained_width =
|
|
142
|
+
if border_box_sizing?(box)
|
|
143
|
+
constrain_css_width(box, content_width + horizontal_box_edges(box), available_width)
|
|
144
|
+
else
|
|
145
|
+
constrain_css_width(box, content_width, available_width)
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
content_width_from_css_width(box, constrained_width)
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def self.constrain_css_width(box, width, available_width)
|
|
152
|
+
min_width = optional_width(box, "min-width", available_width)
|
|
153
|
+
max_width = optional_width(box, "max-width", available_width)
|
|
154
|
+
|
|
155
|
+
width = [width, min_width].max if min_width
|
|
156
|
+
width = [width, max_width].min if max_width
|
|
157
|
+
width
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def self.optional_width(box, property, available_width)
|
|
161
|
+
raw = style_value(box, property)
|
|
162
|
+
return nil if raw.nil? || raw == "none" || raw == "auto"
|
|
163
|
+
|
|
164
|
+
CSS::Values.resolve_length(raw, reference: available_width, default: 0)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def self.content_width_from_css_width(box, width)
|
|
168
|
+
return width unless border_box_sizing?(box)
|
|
169
|
+
|
|
170
|
+
width - horizontal_box_edges(box)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def self.declared_height(box)
|
|
174
|
+
raw = style_value(box, "height")
|
|
175
|
+
return box.height if raw.nil? || raw == "auto"
|
|
176
|
+
|
|
177
|
+
CSS::Values.resolve_length(raw, default: box.height)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def self.content_height_from_css_height(box, height)
|
|
181
|
+
content_height = border_box_sizing?(box) ? height - vertical_box_edges(box) : height
|
|
182
|
+
|
|
183
|
+
[content_height, 0].max
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def self.border_box_sizing?(box)
|
|
187
|
+
style_value(box, "box-sizing") == "border-box"
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def self.horizontal_box_edges(box)
|
|
191
|
+
box.border[:left] + box.border[:right] + box.padding[:left] + box.padding[:right]
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def self.vertical_box_edges(box)
|
|
195
|
+
box.border[:top] + box.border[:bottom] + box.padding[:top] + box.padding[:bottom]
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
def self.preserve_assigned_width?(box, raw_width, available_width)
|
|
199
|
+
return false if CSS::Values.reference_relative?(raw_width)
|
|
200
|
+
|
|
201
|
+
available_width && box.width == available_width
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def self.style_value(box, property)
|
|
205
|
+
value = box.node&.computed_style&.[](property)
|
|
206
|
+
return nil if value.to_s.strip.empty?
|
|
207
|
+
|
|
208
|
+
value
|
|
209
|
+
end
|
|
91
210
|
end
|
|
92
211
|
end
|
|
93
212
|
end
|
|
@@ -18,7 +18,12 @@ module SilkLayout
|
|
|
18
18
|
:explicit_height,
|
|
19
19
|
:flex,
|
|
20
20
|
:display,
|
|
21
|
-
:has_border
|
|
21
|
+
:has_border,
|
|
22
|
+
:replaced,
|
|
23
|
+
:image_source,
|
|
24
|
+
:image_resource,
|
|
25
|
+
:intrinsic_width,
|
|
26
|
+
:intrinsic_height
|
|
22
27
|
|
|
23
28
|
def initialize(node)
|
|
24
29
|
@node = node
|
|
@@ -44,6 +49,11 @@ module SilkLayout
|
|
|
44
49
|
@explicit_height = false
|
|
45
50
|
@flex = {}
|
|
46
51
|
@display = nil
|
|
52
|
+
@replaced = false
|
|
53
|
+
@image_source = nil
|
|
54
|
+
@image_resource = nil
|
|
55
|
+
@intrinsic_width = nil
|
|
56
|
+
@intrinsic_height = nil
|
|
47
57
|
end
|
|
48
58
|
|
|
49
59
|
def add_child(box)
|
|
@@ -65,6 +75,34 @@ module SilkLayout
|
|
|
65
75
|
def border_box_height
|
|
66
76
|
height
|
|
67
77
|
end
|
|
78
|
+
|
|
79
|
+
def content_box_x
|
|
80
|
+
x + border[:left] + padding[:left]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def content_box_y
|
|
84
|
+
y + border[:top] + padding[:top]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def content_box_width
|
|
88
|
+
[width - border[:left] - border[:right] - padding[:left] - padding[:right], 0].max
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def content_box_height
|
|
92
|
+
[height - border[:top] - border[:bottom] - padding[:top] - padding[:bottom], 0].max
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def replaced?
|
|
96
|
+
replaced
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def image?
|
|
100
|
+
!image_resource.nil?
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def image_path
|
|
104
|
+
image_resource&.path
|
|
105
|
+
end
|
|
68
106
|
end
|
|
69
107
|
|
|
70
108
|
class BlockBox < Box; end
|
|
@@ -3,10 +3,15 @@
|
|
|
3
3
|
module SilkLayout
|
|
4
4
|
module Layout
|
|
5
5
|
class Context
|
|
6
|
-
attr_reader :width
|
|
6
|
+
attr_reader :width, :page_size
|
|
7
7
|
|
|
8
|
-
def initialize(width:)
|
|
8
|
+
def initialize(width:, page_size: nil)
|
|
9
9
|
@width = width
|
|
10
|
+
@page_size = page_size
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def viewport_width
|
|
14
|
+
width
|
|
10
15
|
end
|
|
11
16
|
end
|
|
12
17
|
end
|
|
@@ -5,18 +5,28 @@ module SilkLayout
|
|
|
5
5
|
class Engine
|
|
6
6
|
DEFAULT_VIEWPORT_WIDTH = 800
|
|
7
7
|
|
|
8
|
-
def self.layout(dom, css_rules, viewport_width:
|
|
8
|
+
def self.layout(dom, css_rules, viewport_width: nil, page_size: nil)
|
|
9
9
|
CSS::Cascade.apply(dom, css_rules)
|
|
10
10
|
|
|
11
11
|
box_tree = FormattingBuilder.build(dom)
|
|
12
12
|
|
|
13
13
|
root = Root.find(box_tree)
|
|
14
14
|
|
|
15
|
-
context = Context.new(width: viewport_width)
|
|
15
|
+
context = Context.new(width: viewport_width || page_width(page_size) || DEFAULT_VIEWPORT_WIDTH, page_size: page_size)
|
|
16
16
|
BlockLayout.layout(root, context)
|
|
17
17
|
|
|
18
18
|
root
|
|
19
19
|
end
|
|
20
|
+
|
|
21
|
+
def self.page_width(page_size)
|
|
22
|
+
case page_size
|
|
23
|
+
when Array
|
|
24
|
+
page_size[0]
|
|
25
|
+
when Hash
|
|
26
|
+
page_size[:width] || page_size["width"]
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
private_class_method :page_width
|
|
20
30
|
end
|
|
21
31
|
end
|
|
22
32
|
end
|
|
@@ -19,6 +19,7 @@ module SilkLayout
|
|
|
19
19
|
"strong" => "inline",
|
|
20
20
|
"em" => "inline",
|
|
21
21
|
"br" => "inline",
|
|
22
|
+
"img" => "inline",
|
|
22
23
|
"h1" => "block",
|
|
23
24
|
"h2" => "block",
|
|
24
25
|
"h3" => "block",
|
|
@@ -131,10 +132,75 @@ module SilkLayout
|
|
|
131
132
|
|
|
132
133
|
box.background_color = color(background_color(style))
|
|
133
134
|
box.flex = flex_values(style)
|
|
135
|
+
apply_image_data(box, node, style)
|
|
134
136
|
|
|
135
137
|
box
|
|
136
138
|
end
|
|
137
139
|
|
|
140
|
+
def apply_image_data(box, node, style)
|
|
141
|
+
return unless node.tag == "img"
|
|
142
|
+
|
|
143
|
+
source = node.resolved_source_url || node.attributes["src"]
|
|
144
|
+
image = SilkLayout::Resource::Image.load(source)
|
|
145
|
+
|
|
146
|
+
box.replaced = true
|
|
147
|
+
box.image_source = source.to_s
|
|
148
|
+
box.image_resource = image
|
|
149
|
+
box.intrinsic_width = image&.width
|
|
150
|
+
box.intrinsic_height = image&.height
|
|
151
|
+
|
|
152
|
+
box.width, box.height = replaced_dimensions(style, node.attributes, image)
|
|
153
|
+
box.explicit_width = true
|
|
154
|
+
box.explicit_height = true
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def replaced_dimensions(style, attributes, image)
|
|
158
|
+
attr_width = html_dimension(attributes["width"])
|
|
159
|
+
attr_height = html_dimension(attributes["height"])
|
|
160
|
+
css_width = css_replaced_dimension(style, "width")
|
|
161
|
+
css_height = css_replaced_dimension(style, "height")
|
|
162
|
+
|
|
163
|
+
width = css_width.nil? ? attr_width : css_width
|
|
164
|
+
height = css_height.nil? ? attr_height : css_height
|
|
165
|
+
ratio = image&.aspect_ratio || aspect_ratio(attr_width, attr_height)
|
|
166
|
+
|
|
167
|
+
if width.nil? && height.nil?
|
|
168
|
+
width = image&.width || 0
|
|
169
|
+
height = image&.height || 0
|
|
170
|
+
elsif width.nil?
|
|
171
|
+
width = ratio ? height * ratio : (image&.width || 0)
|
|
172
|
+
elsif height.nil?
|
|
173
|
+
height = ratio ? width / ratio : (image&.height || 0)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
[width.to_f, height.to_f]
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def html_dimension(value)
|
|
180
|
+
raw = value.to_s.strip
|
|
181
|
+
return nil if raw.empty?
|
|
182
|
+
|
|
183
|
+
raw = raw.delete_suffix("px")
|
|
184
|
+
return nil unless raw.match?(/\A[-+]?\d*\.?\d+\z/)
|
|
185
|
+
|
|
186
|
+
raw.to_f
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def aspect_ratio(width, height)
|
|
190
|
+
return nil unless width&.positive? && height&.positive?
|
|
191
|
+
|
|
192
|
+
width.to_f / height
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def css_replaced_dimension(style, property)
|
|
196
|
+
return nil unless style.public_send("explicit_#{property}?")
|
|
197
|
+
|
|
198
|
+
raw = style[property]
|
|
199
|
+
return nil if CSS::Values.reference_relative?(raw)
|
|
200
|
+
|
|
201
|
+
CSS::Values.resolve_length(raw, default: nil)
|
|
202
|
+
end
|
|
203
|
+
|
|
138
204
|
def build_text(node)
|
|
139
205
|
return nil unless node.text
|
|
140
206
|
|
|
@@ -261,20 +327,7 @@ module SilkLayout
|
|
|
261
327
|
end
|
|
262
328
|
|
|
263
329
|
def expanded_values(value)
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
case tokens.length
|
|
267
|
-
when 0
|
|
268
|
-
{top: nil, right: nil, bottom: nil, left: nil}
|
|
269
|
-
when 1
|
|
270
|
-
{top: tokens[0], right: tokens[0], bottom: tokens[0], left: tokens[0]}
|
|
271
|
-
when 2
|
|
272
|
-
{top: tokens[0], right: tokens[1], bottom: tokens[0], left: tokens[1]}
|
|
273
|
-
when 3
|
|
274
|
-
{top: tokens[0], right: tokens[1], bottom: tokens[2], left: tokens[1]}
|
|
275
|
-
else
|
|
276
|
-
{top: tokens[0], right: tokens[1], bottom: tokens[2], left: tokens[3]}
|
|
277
|
-
end
|
|
330
|
+
CSS::Values.expanded_edges(value)
|
|
278
331
|
end
|
|
279
332
|
|
|
280
333
|
def border_shorthand(value)
|
|
@@ -371,9 +424,10 @@ module SilkLayout
|
|
|
371
424
|
end
|
|
372
425
|
|
|
373
426
|
def color(value)
|
|
374
|
-
|
|
427
|
+
parsed = SilkLayout::CSS::Color.parse(value)
|
|
428
|
+
return nil unless parsed
|
|
375
429
|
|
|
376
|
-
|
|
430
|
+
parsed.to_sym
|
|
377
431
|
end
|
|
378
432
|
|
|
379
433
|
def number(value, default)
|
|
@@ -386,7 +440,7 @@ module SilkLayout
|
|
|
386
440
|
end
|
|
387
441
|
|
|
388
442
|
def split_tokens(value)
|
|
389
|
-
|
|
443
|
+
CSS::Values.split_tokens(value)
|
|
390
444
|
end
|
|
391
445
|
|
|
392
446
|
def numeric?(value)
|
|
@@ -415,8 +469,8 @@ module SilkLayout
|
|
|
415
469
|
def color_token?(value)
|
|
416
470
|
raw = value.to_s.strip
|
|
417
471
|
return false if raw.empty?
|
|
418
|
-
return true if
|
|
419
|
-
return false if %w[none
|
|
472
|
+
return true if SilkLayout::CSS::Color.parse(raw)
|
|
473
|
+
return false if %w[none inherit initial unset].include?(raw)
|
|
420
474
|
|
|
421
475
|
!border_width?(raw) && !border_style?(raw) && !raw.include?("(") && !raw.include?("/")
|
|
422
476
|
end
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module SilkLayout
|
|
4
4
|
module Layout
|
|
5
5
|
class InlineFormatter
|
|
6
|
-
Fragment = Struct.new(:box, :text, :width, :height, :break_after) do
|
|
6
|
+
Fragment = Struct.new(:box, :text, :width, :height, :break_after, :ascender) do
|
|
7
7
|
def whitespace?
|
|
8
8
|
text&.match?(/\A\s+\z/)
|
|
9
9
|
end
|
|
@@ -11,7 +11,7 @@ module SilkLayout
|
|
|
11
11
|
|
|
12
12
|
class << self
|
|
13
13
|
def layout(inline_children, available_width, parent_x, parent_y)
|
|
14
|
-
fragments = flatten(inline_children)
|
|
14
|
+
fragments = flatten(inline_children, available_width)
|
|
15
15
|
lines = []
|
|
16
16
|
current_fragments = []
|
|
17
17
|
current_y = parent_y
|
|
@@ -61,12 +61,13 @@ module SilkLayout
|
|
|
61
61
|
|
|
62
62
|
line = LineBox.new
|
|
63
63
|
cursor_x = 0
|
|
64
|
-
baseline = fragments.map { |fragment| fragment
|
|
64
|
+
baseline = fragments.map { |fragment| fragment_ascender(fragment) }.max || 0
|
|
65
|
+
descent = fragments.map { |fragment| fragment.height - fragment_ascender(fragment) }.max || 0
|
|
65
66
|
|
|
66
67
|
fragments.each do |fragment|
|
|
67
68
|
child = fragment.box
|
|
68
69
|
child.x = parent_x + cursor_x
|
|
69
|
-
child.y = parent_y + baseline -
|
|
70
|
+
child.y = parent_y + baseline - fragment_ascender(fragment)
|
|
70
71
|
child.width = fragment.width
|
|
71
72
|
child.height = fragment.height
|
|
72
73
|
cursor_x += child.width
|
|
@@ -74,7 +75,7 @@ module SilkLayout
|
|
|
74
75
|
end
|
|
75
76
|
|
|
76
77
|
line.width = cursor_x
|
|
77
|
-
line.height =
|
|
78
|
+
line.height = baseline + descent
|
|
78
79
|
line.x = parent_x
|
|
79
80
|
line.y = parent_y
|
|
80
81
|
line
|
|
@@ -86,25 +87,98 @@ module SilkLayout
|
|
|
86
87
|
trimmed
|
|
87
88
|
end
|
|
88
89
|
|
|
89
|
-
def flatten(boxes)
|
|
90
|
-
boxes.flat_map { |box| flatten_box(box) }
|
|
90
|
+
def flatten(boxes, available_width)
|
|
91
|
+
boxes.flat_map { |box| flatten_box(box, available_width) }
|
|
91
92
|
end
|
|
92
93
|
|
|
93
|
-
def flatten_box(box)
|
|
94
|
+
def flatten_box(box, available_width)
|
|
94
95
|
case box
|
|
95
96
|
when TextBox
|
|
96
97
|
tokenize_text(box)
|
|
97
98
|
when InlineBox
|
|
98
|
-
if box.
|
|
99
|
+
if box.respond_to?(:replaced?) && box.replaced?
|
|
100
|
+
[replaced_fragment(box, available_width)]
|
|
101
|
+
elsif box.node&.tag == "br"
|
|
99
102
|
[Fragment.new(box: box, break_after: true)]
|
|
100
103
|
else
|
|
101
|
-
flatten(box.children)
|
|
104
|
+
flatten(box.children, available_width)
|
|
102
105
|
end
|
|
103
106
|
else
|
|
104
107
|
[]
|
|
105
108
|
end
|
|
106
109
|
end
|
|
107
110
|
|
|
111
|
+
def replaced_fragment(box, available_width)
|
|
112
|
+
content_width, content_height = replaced_content_size(box, available_width)
|
|
113
|
+
width = content_width + horizontal_box_edges(box)
|
|
114
|
+
height = content_height + vertical_box_edges(box)
|
|
115
|
+
|
|
116
|
+
Fragment.new(
|
|
117
|
+
box: box,
|
|
118
|
+
width: width,
|
|
119
|
+
height: height,
|
|
120
|
+
ascender: height
|
|
121
|
+
)
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def replaced_content_size(box, available_width)
|
|
125
|
+
width = resolved_replaced_width(box, available_width)
|
|
126
|
+
height = resolved_replaced_height(box)
|
|
127
|
+
ratio = aspect_ratio(box)
|
|
128
|
+
|
|
129
|
+
if width.nil? && height
|
|
130
|
+
width = ratio ? height * ratio : box.width
|
|
131
|
+
elsif height.nil? && width
|
|
132
|
+
height = ratio ? width / ratio : box.height
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
[width || box.width, height || box.height].map { |value| [value, 0].max }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def resolved_replaced_width(box, available_width)
|
|
139
|
+
raw = style_value(box, "width")
|
|
140
|
+
return box.width if raw.nil? || raw == "auto"
|
|
141
|
+
|
|
142
|
+
width = CSS::Values.resolve_length(raw, reference: available_width, default: box.width)
|
|
143
|
+
border_box_sizing?(box) ? width - horizontal_box_edges(box) : width
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def resolved_replaced_height(box)
|
|
147
|
+
raw = style_value(box, "height")
|
|
148
|
+
return nil if raw.nil? || raw == "auto"
|
|
149
|
+
|
|
150
|
+
height = CSS::Values.resolve_length(raw, default: box.height)
|
|
151
|
+
border_box_sizing?(box) ? height - vertical_box_edges(box) : height
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def aspect_ratio(box)
|
|
155
|
+
image_ratio = box.image_resource&.aspect_ratio
|
|
156
|
+
return image_ratio if image_ratio
|
|
157
|
+
|
|
158
|
+
return nil unless box.width.positive? && box.height.positive?
|
|
159
|
+
|
|
160
|
+
box.width.to_f / box.height
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def border_box_sizing?(box)
|
|
164
|
+
style_value(box, "box-sizing") == "border-box"
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def horizontal_box_edges(box)
|
|
168
|
+
box.border[:left] + box.border[:right] + box.padding[:left] + box.padding[:right]
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def vertical_box_edges(box)
|
|
172
|
+
box.border[:top] + box.border[:bottom] + box.padding[:top] + box.padding[:bottom]
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def style_value(box, property)
|
|
176
|
+
value = box.node&.computed_style&.[](property)
|
|
177
|
+
return nil if value.to_s.strip.empty?
|
|
178
|
+
|
|
179
|
+
value
|
|
180
|
+
end
|
|
181
|
+
|
|
108
182
|
def tokenize_text(box)
|
|
109
183
|
box.text.scan(/\s+|\S+/).map do |token|
|
|
110
184
|
fragment_box = box.clone_with_text(token)
|
|
@@ -126,6 +200,10 @@ module SilkLayout
|
|
|
126
200
|
font_style: box.font_style
|
|
127
201
|
)
|
|
128
202
|
end
|
|
203
|
+
|
|
204
|
+
def fragment_ascender(fragment)
|
|
205
|
+
fragment.ascender || (fragment.box.is_a?(TextBox) ? fragment.box.ascender : fragment.height)
|
|
206
|
+
end
|
|
129
207
|
end
|
|
130
208
|
end
|
|
131
209
|
end
|
|
@@ -4,11 +4,10 @@ module SilkLayout
|
|
|
4
4
|
module Layout
|
|
5
5
|
class Root
|
|
6
6
|
def self.find(box)
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
current
|
|
7
|
+
return nil unless box
|
|
8
|
+
return box unless box.node&.tag == "html"
|
|
9
|
+
|
|
10
|
+
box.children.find { |child| child.node&.tag == "body" } || box
|
|
12
11
|
end
|
|
13
12
|
end
|
|
14
13
|
end
|
|
@@ -9,6 +9,7 @@ module SilkLayout
|
|
|
9
9
|
|
|
10
10
|
def self.paint_box(canvas, box, page_height_pt)
|
|
11
11
|
draw_background(canvas, box, page_height_pt)
|
|
12
|
+
paint_image(canvas, box, page_height_pt) if box.respond_to?(:image?) && box.image?
|
|
12
13
|
draw_borders(canvas, box, page_height_pt)
|
|
13
14
|
|
|
14
15
|
if box.is_a?(SilkLayout::Layout::TextBox)
|
|
@@ -37,6 +38,21 @@ module SilkLayout
|
|
|
37
38
|
canvas.text(box.text, at: [x_pt, y_pt])
|
|
38
39
|
end
|
|
39
40
|
|
|
41
|
+
def self.paint_image(canvas, box, page_height_pt)
|
|
42
|
+
return unless box.image_path && File.file?(box.image_path)
|
|
43
|
+
|
|
44
|
+
width = box.content_box_width
|
|
45
|
+
height = box.content_box_height
|
|
46
|
+
return unless width.positive? && height.positive?
|
|
47
|
+
|
|
48
|
+
x_pt = PdfRenderer.px_to_pt(box.content_box_x)
|
|
49
|
+
y_pt = page_height_pt - PdfRenderer.px_to_pt(box.content_box_y + height)
|
|
50
|
+
width_pt = PdfRenderer.px_to_pt(width)
|
|
51
|
+
height_pt = PdfRenderer.px_to_pt(height)
|
|
52
|
+
|
|
53
|
+
canvas.image(box.image_path, at: [x_pt, y_pt], width: width_pt, height: height_pt)
|
|
54
|
+
end
|
|
55
|
+
|
|
40
56
|
def self.draw_background(canvas, box, page_height_pt)
|
|
41
57
|
return if box.is_a?(SilkLayout::Layout::AnonymousBlockBox)
|
|
42
58
|
return unless box.respond_to?(:background_color) && box.background_color
|
|
@@ -183,20 +199,7 @@ module SilkLayout
|
|
|
183
199
|
end
|
|
184
200
|
|
|
185
201
|
def self.rgb_color(color)
|
|
186
|
-
|
|
187
|
-
return nil if normalized.empty? || normalized == "transparent"
|
|
188
|
-
|
|
189
|
-
return hex_color(normalized) if normalized.start_with?("#")
|
|
190
|
-
|
|
191
|
-
NAMED_COLORS[normalized.to_sym]
|
|
192
|
-
end
|
|
193
|
-
|
|
194
|
-
def self.hex_color(color)
|
|
195
|
-
hex = color.delete_prefix("#")
|
|
196
|
-
hex = hex.chars.flat_map { |char| [char, char] }.join if hex.length == 3
|
|
197
|
-
return nil unless hex.match?(/\A[0-9a-f]{6}\z/)
|
|
198
|
-
|
|
199
|
-
hex.scan(/../).map { |component| component.to_i(16) }
|
|
202
|
+
SilkLayout::CSS::Color.rgb(color)
|
|
200
203
|
end
|
|
201
204
|
|
|
202
205
|
def self.draw_corner(canvas, x, y, w, h, vertical_color, horizontal_color, kind)
|
|
@@ -233,15 +236,6 @@ module SilkLayout
|
|
|
233
236
|
end
|
|
234
237
|
|
|
235
238
|
private_class_method :paint_box
|
|
236
|
-
|
|
237
|
-
NAMED_COLORS = {
|
|
238
|
-
black: [0, 0, 0],
|
|
239
|
-
blue: [0, 0, 255],
|
|
240
|
-
green: [0, 128, 0],
|
|
241
|
-
lightblue: [173, 216, 230],
|
|
242
|
-
red: [255, 0, 0],
|
|
243
|
-
white: [255, 255, 255]
|
|
244
|
-
}.freeze
|
|
245
239
|
end
|
|
246
240
|
end
|
|
247
241
|
end
|