-
-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathdocument_renderer.rb
More file actions
171 lines (144 loc) · 5.49 KB
/
document_renderer.rb
File metadata and controls
171 lines (144 loc) · 5.49 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
# frozen_string_literal: true
module PrawnHtml
class DocumentRenderer
NEW_LINE = { text: "\n" }.freeze
SPACE = { text: ' ' }.freeze
# Init the DocumentRenderer
#
# @param pdf [PdfWrapper] target PDF wrapper
def initialize(pdf)
@before_content = []
@buffer = []
@context = Context.new
@last_margin = 0
@last_text = ''
@last_tag_open = false
@pdf = pdf
end
# On tag close callback
#
# @param element [Tag] closing element wrapper
def on_tag_close(element)
render_if_needed(element)
apply_tag_close_styles(element)
context.remove_last
@last_tag_open = false
@last_text = ''
end
# On tag open callback
#
# @param tag_name [String] the tag name of the opening element
# @param attributes [Hash] an hash of the element attributes
# @param element_styles [String] document styles to apply to the element
#
# @return [Tag] the opening element wrapper
def on_tag_open(tag_name, attributes:, element_styles: '')
tag_class = Tag.class_for(tag_name)
return unless tag_class
options = { width: pdf.page_width, height: pdf.page_height }
tag_class.new(tag_name, attributes: attributes, options: options).tap do |element|
setup_element(element, element_styles: element_styles)
@before_content.push(element.before_content) if element.respond_to?(:before_content)
@last_tag_open = true
end
end
# On text node callback
#
# @param content [String] the text node content
#
# @return [NilClass] nil value (=> no element)
def on_text_node(content)
return if context.previous_tag&.block? && content.match?(/\A\s*\Z/)
text = prepare_text(content)
buffer << context.merged_styles.merge(text: text) unless text.empty?
context.last_text_node = true
nil
end
# Render the buffer content to the PDF document
def render
return if buffer.empty?
content = prepare_content(buffer.dup, context.block_styles)
if context.current_table
td_content = content.dig(:buffer, 0, :text)
context.current_table.update_content(td_content)
else
pdf.puts(content[:buffer], content[:options], left_indent: content[:left_indent], bounding_box: content[:bounds])
end
buffer.clear
@last_margin = 0
end
alias_method :flush, :render
private
attr_reader :buffer, :context, :last_margin, :pdf
def setup_element(element, element_styles:)
render_if_needed(element)
context.add(element)
element.process_styles(element_styles: element_styles)
apply_tag_open_styles(element)
element.custom_render(pdf, context) if element.respond_to?(:custom_render)
end
def render_if_needed(element)
render_needed = element&.block? && buffer.any? && buffer.last != NEW_LINE
return false unless render_needed
render
true
end
def apply_tag_close_styles(element)
tag_styles = element.tag_closing(context: context)
pdf.table(element.table_data) if element.is_a?(Tags::Table)
@last_margin = tag_styles[:margin_bottom].to_f
pdf.advance_cursor(last_margin + tag_styles[:padding_bottom].to_f)
pdf.start_new_page if tag_styles[:break_after]
end
def apply_tag_open_styles(element)
tag_styles = element.tag_opening(context: context)
move_down = (tag_styles[:margin_top].to_f - last_margin) + tag_styles[:padding_top].to_f
pdf.advance_cursor(move_down) if move_down > 0
pdf.start_new_page if tag_styles[:break_before]
end
def prepare_text(content)
text = @before_content.any? ? ::Oga::HTML::Entities.decode(@before_content.join) : ''
@before_content.clear
return (@last_text = text + content) if context.white_space_pre?
content = content.lstrip if @last_text[-1] == ' ' || @last_tag_open
text += content.tr("\n", ' ').squeeze(' ')
@last_text = text
end
def prepare_content(buffer, block_styles)
apply_callbacks(buffer)
left_indent = block_styles[:margin_left].to_f + block_styles[:padding_left].to_f
options = block_styles.slice(:align, :indent_paragraphs, :leading, :mode, :padding_left)
options[:leading] = adjust_leading(buffer, options[:leading])
{ buffer: buffer, options: options, left_indent: left_indent, bounds: bounds(buffer, options, block_styles) }
end
def apply_callbacks(buffer)
buffer.select { |item| item[:callback] }.each do |item|
callback, arg = item[:callback]
callback_class = Tag::CALLBACKS[callback]
item[:callback] = callback_class.new(pdf, arg)
end
end
def adjust_leading(buffer, leading)
return leading if leading
leadings = buffer.map do |item|
(item[:size] || Context::DEFAULT_STYLES[:size]) * (ADJUST_LEADING[item[:font]] || ADJUST_LEADING[nil])
end
leadings.max.round(4)
end
def bounds(buffer, options, block_styles)
return unless block_styles[:position] == :absolute
x = if block_styles.include?(:right)
x1 = pdf.calc_buffer_width(buffer) + block_styles[:right]
x1 < pdf.page_width ? (pdf.page_width - x1) : 0
else
block_styles[:left] || 0
end
y = if block_styles.include?(:bottom)
pdf.calc_buffer_height(buffer, options) + block_styles[:bottom]
else
pdf.page_height - (block_styles[:top] || 0)
end
[[x, y], { width: pdf.page_width - x }]
end
end
end