prawnx 0.1.1
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 +7 -0
- data/COPYING +2 -0
- data/GPLv2 +339 -0
- data/GPLv3 +674 -0
- data/LICENSE +65 -0
- data/LICENSE-prawn-svg +21 -0
- data/README.md +107 -0
- data/data/fonts/Courier-Bold.afm +342 -0
- data/data/fonts/Courier-BoldOblique.afm +342 -0
- data/data/fonts/Courier-Oblique.afm +342 -0
- data/data/fonts/Courier.afm +342 -0
- data/data/fonts/Helvetica-Bold.afm +2827 -0
- data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
- data/data/fonts/Helvetica-Oblique.afm +3051 -0
- data/data/fonts/Helvetica.afm +3051 -0
- data/data/fonts/MustRead.html +19 -0
- data/data/fonts/Symbol.afm +213 -0
- data/data/fonts/Times-Bold.afm +2588 -0
- data/data/fonts/Times-BoldItalic.afm +2384 -0
- data/data/fonts/Times-Italic.afm +2667 -0
- data/data/fonts/Times-Roman.afm +2419 -0
- data/data/fonts/ZapfDingbats.afm +225 -0
- data/lib/pdf/core.rb +4 -0
- data/lib/prawn/measurement_extensions.rb +4 -0
- data/lib/prawn/svg/attributes/clip_path.rb +46 -0
- data/lib/prawn/svg/attributes/mask.rb +16 -0
- data/lib/prawn/svg/attributes/opacity.rb +30 -0
- data/lib/prawn/svg/attributes/space.rb +10 -0
- data/lib/prawn/svg/attributes/stroke.rb +50 -0
- data/lib/prawn/svg/attributes/transform.rb +10 -0
- data/lib/prawn/svg/attributes.rb +6 -0
- data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
- data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
- data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
- data/lib/prawn/svg/calculators/path_length.rb +168 -0
- data/lib/prawn/svg/calculators/pixels.rb +57 -0
- data/lib/prawn/svg/color.rb +240 -0
- data/lib/prawn/svg/css/font_face_parser.rb +72 -0
- data/lib/prawn/svg/css/font_family_parser.rb +39 -0
- data/lib/prawn/svg/css/font_parser.rb +46 -0
- data/lib/prawn/svg/css/selector_parser.rb +178 -0
- data/lib/prawn/svg/css/stylesheets.rb +233 -0
- data/lib/prawn/svg/css/values_parser.rb +68 -0
- data/lib/prawn/svg/document.rb +170 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +312 -0
- data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
- data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
- data/lib/prawn/svg/elements/circle.rb +25 -0
- data/lib/prawn/svg/elements/clip_path.rb +26 -0
- data/lib/prawn/svg/elements/container.rb +10 -0
- data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
- data/lib/prawn/svg/elements/ellipse.rb +22 -0
- data/lib/prawn/svg/elements/gradient.rb +184 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +177 -0
- data/lib/prawn/svg/elements/line.rb +27 -0
- data/lib/prawn/svg/elements/marker.rb +72 -0
- data/lib/prawn/svg/elements/mask.rb +65 -0
- data/lib/prawn/svg/elements/path.rb +294 -0
- data/lib/prawn/svg/elements/pattern.rb +150 -0
- data/lib/prawn/svg/elements/polygon.rb +25 -0
- data/lib/prawn/svg/elements/polyline.rb +23 -0
- data/lib/prawn/svg/elements/rect.rb +64 -0
- data/lib/prawn/svg/elements/root.rb +22 -0
- data/lib/prawn/svg/elements/switch.rb +92 -0
- data/lib/prawn/svg/elements/text.rb +99 -0
- data/lib/prawn/svg/elements/text_component.rb +346 -0
- data/lib/prawn/svg/elements/text_node.rb +308 -0
- data/lib/prawn/svg/elements/text_path.rb +153 -0
- data/lib/prawn/svg/elements/use.rb +142 -0
- data/lib/prawn/svg/elements/viewport.rb +29 -0
- data/lib/prawn/svg/elements.rb +46 -0
- data/lib/prawn/svg/extension.rb +25 -0
- data/lib/prawn/svg/font.rb +20 -0
- data/lib/prawn/svg/font_metrics.rb +144 -0
- data/lib/prawn/svg/font_registry.rb +315 -0
- data/lib/prawn/svg/funciri.rb +14 -0
- data/lib/prawn/svg/gradient_renderer.rb +332 -0
- data/lib/prawn/svg/gradients.rb +53 -0
- data/lib/prawn/svg/interface.rb +72 -0
- data/lib/prawn/svg/length.rb +43 -0
- data/lib/prawn/svg/link_renderer.rb +40 -0
- data/lib/prawn/svg/loaders/data.rb +17 -0
- data/lib/prawn/svg/loaders/file.rb +111 -0
- data/lib/prawn/svg/loaders/web.rb +24 -0
- data/lib/prawn/svg/paint.rb +67 -0
- data/lib/prawn/svg/pathable.rb +153 -0
- data/lib/prawn/svg/pattern_renderer.rb +112 -0
- data/lib/prawn/svg/pdfmatrix.rb +37 -0
- data/lib/prawn/svg/percentage.rb +24 -0
- data/lib/prawn/svg/properties.rb +329 -0
- data/lib/prawn/svg/renderer.rb +249 -0
- data/lib/prawn/svg/state.rb +24 -0
- data/lib/prawn/svg/transform_parser.rb +78 -0
- data/lib/prawn/svg/ttc.rb +35 -0
- data/lib/prawn/svg/ttf.rb +95 -0
- data/lib/prawn/svg/url_loader.rb +46 -0
- data/lib/prawn/svg/version.rb +5 -0
- data/lib/prawn/table.rb +4 -0
- data/lib/prawn-svg.rb +53 -0
- data/lib/prawn.rb +10 -0
- data/lib/prawnx/annotations.rb +21 -0
- data/lib/prawnx/document/bounding_box.rb +193 -0
- data/lib/prawnx/document/column_box.rb +91 -0
- data/lib/prawnx/document/internals.rb +144 -0
- data/lib/prawnx/document/span.rb +25 -0
- data/lib/prawnx/document.rb +253 -0
- data/lib/prawnx/encoding.rb +29 -0
- data/lib/prawnx/errors.rb +37 -0
- data/lib/prawnx/font.rb +214 -0
- data/lib/prawnx/fonts/afm.rb +171 -0
- data/lib/prawnx/fonts/otf.rb +50 -0
- data/lib/prawnx/fonts/ttf.rb +410 -0
- data/lib/prawnx/graphic_state.rb +41 -0
- data/lib/prawnx/graphics/color.rb +148 -0
- data/lib/prawnx/graphics/patterns.rb +100 -0
- data/lib/prawnx/graphics/style.rb +171 -0
- data/lib/prawnx/graphics.rb +208 -0
- data/lib/prawnx/grid.rb +216 -0
- data/lib/prawnx/images/image.rb +244 -0
- data/lib/prawnx/images/png_decoder.rb +65 -0
- data/lib/prawnx/images.rb +115 -0
- data/lib/prawnx/measurement_extensions.rb +40 -0
- data/lib/prawnx/measurements.rb +62 -0
- data/lib/prawnx/outline.rb +146 -0
- data/lib/prawnx/page.rb +125 -0
- data/lib/prawnx/page_geometry.rb +27 -0
- data/lib/prawnx/pdf/outline_node.rb +53 -0
- data/lib/prawnx/pdf/reference.rb +31 -0
- data/lib/prawnx/pdf/writer.rb +105 -0
- data/lib/prawnx/pdf.rb +66 -0
- data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
- data/lib/prawnx/pdf_a.rb +130 -0
- data/lib/prawnx/pdf_core.rb +25 -0
- data/lib/prawnx/repeater.rb +79 -0
- data/lib/prawnx/security.rb +108 -0
- data/lib/prawnx/stamp.rb +74 -0
- data/lib/prawnx/table/cell/kinds.rb +246 -0
- data/lib/prawnx/table/cell.rb +331 -0
- data/lib/prawnx/table/cells.rb +208 -0
- data/lib/prawnx/table.rb +296 -0
- data/lib/prawnx/text/box.rb +16 -0
- data/lib/prawnx/text/formatted/arranger.rb +217 -0
- data/lib/prawnx/text/formatted/box.rb +312 -0
- data/lib/prawnx/text/formatted/fragment.rb +205 -0
- data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
- data/lib/prawnx/text/formatted/parser.rb +149 -0
- data/lib/prawnx/text/formatted/wrap.rb +103 -0
- data/lib/prawnx/text.rb +281 -0
- data/lib/prawnx/transformation_stack.rb +43 -0
- data/lib/prawnx/true_type/cff_subset.rb +157 -0
- data/lib/prawnx/true_type/file.rb +371 -0
- data/lib/prawnx/true_type/open_type_layout.rb +189 -0
- data/lib/prawnx/true_type/simple_subsets.rb +94 -0
- data/lib/prawnx/true_type/subset.rb +187 -0
- data/lib/prawnx/version.rb +7 -0
- data/lib/prawnx/view.rb +28 -0
- data/lib/prawnx.rb +102 -0
- metadata +251 -0
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
module PrawnX
|
|
6
|
+
module Text
|
|
7
|
+
module Formatted
|
|
8
|
+
# Fills one line with as many words of the arranger's fragments as fit, breaking after spaces, hyphens and
|
|
9
|
+
# soft hyphens, and inside a word only when the line holds a single one.
|
|
10
|
+
class LineWrap
|
|
11
|
+
# Characters and patterns of one encoding, computed once: Prawn rebuilt them for every line.
|
|
12
|
+
Breaks = Data.define(:soft_hyphen, :zero_width_space, :scan_pattern, :word_division_pattern, :ends_with_break_pattern,
|
|
13
|
+
:last_word_pattern, :begins_with_break_pattern)
|
|
14
|
+
|
|
15
|
+
BREAKS_BY_ENCODING = Hash.new do |cache, encoding|
|
|
16
|
+
cache[encoding] = breaks_for(encoding)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.encode_or_nil(character, encoding)
|
|
20
|
+
character.encode(encoding)
|
|
21
|
+
rescue ::Encoding::InvalidByteSequenceError, ::Encoding::UndefinedConversionError
|
|
22
|
+
nil
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.breaks_for(encoding)
|
|
26
|
+
soft_hyphen = encode_or_nil(SHY, encoding)
|
|
27
|
+
zero_width_space = encode_or_nil(ZWSP, encoding)
|
|
28
|
+
whitespace = " \t#{zero_width_space}".encode(encoding)
|
|
29
|
+
break_characters = [whitespace, soft_hyphen, "-"].join
|
|
30
|
+
patterns = ["[^#{break_characters}]+#{soft_hyphen}", "[^#{break_characters}]+-+", "[^#{break_characters}]+", "[#{whitespace}]+",
|
|
31
|
+
"-+[^#{break_characters}]*", soft_hyphen.to_s]
|
|
32
|
+
Breaks.new(
|
|
33
|
+
soft_hyphen: soft_hyphen&.freeze,
|
|
34
|
+
zero_width_space: zero_width_space&.freeze,
|
|
35
|
+
scan_pattern: Regexp.new(patterns.map { it.encode(encoding) }.join("|")),
|
|
36
|
+
word_division_pattern: Regexp.union([*["\t", "\n", "\v", "\r", " "].map { it.encode(encoding) }, zero_width_space, soft_hyphen, "-"].compact),
|
|
37
|
+
ends_with_break_pattern: Regexp.new("[#{break_characters}]$"),
|
|
38
|
+
last_word_pattern: Regexp.new("[^#{break_characters}]*$"),
|
|
39
|
+
begins_with_break_pattern: Regexp.new("^[#{break_characters}]"),
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
attr_reader :space_count
|
|
44
|
+
|
|
45
|
+
def width
|
|
46
|
+
@accumulated_width || 0
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def paragraph_finished?
|
|
50
|
+
@newline_encountered || next_string_newline? || @arranger.finished?
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def tokenize(fragment)
|
|
54
|
+
fragment.scan(BREAKS_BY_ENCODING[fragment.encoding].scan_pattern)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def wrap_line(options)
|
|
58
|
+
initialize_line(options)
|
|
59
|
+
while (fragment = @arranger.next_string)
|
|
60
|
+
@fragment_output = +""
|
|
61
|
+
fragment.lstrip! if first_fragment_on_this_line?(fragment)
|
|
62
|
+
next if empty_line?(fragment)
|
|
63
|
+
break unless apply_font_settings_and_add_fragment_to_line(fragment)
|
|
64
|
+
end
|
|
65
|
+
@arranger.finalize_line
|
|
66
|
+
@accumulated_width = @arranger.line_width
|
|
67
|
+
@space_count = @arranger.space_count
|
|
68
|
+
@arranger.line
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
def first_fragment_on_this_line?(fragment)
|
|
74
|
+
line_empty? && fragment != "\n"
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def empty_line?(fragment)
|
|
78
|
+
empty = line_empty? && fragment.empty? && next_string_newline?
|
|
79
|
+
@arranger.update_last_string("", "", BREAKS_BY_ENCODING[fragment.encoding].soft_hyphen) if empty
|
|
80
|
+
empty
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def next_string_newline?
|
|
84
|
+
@arranger.preview_next_string == "\n"
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def apply_font_settings_and_add_fragment_to_line(fragment)
|
|
88
|
+
added = nil
|
|
89
|
+
@arranger.apply_font_settings { added = add_fragment_to_line(fragment) }
|
|
90
|
+
added
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def add_fragment_to_line(fragment)
|
|
94
|
+
case fragment
|
|
95
|
+
when "" then return true
|
|
96
|
+
when "\n"
|
|
97
|
+
@newline_encountered = true
|
|
98
|
+
return false
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
breaks = BREAKS_BY_ENCODING[fragment.encoding]
|
|
102
|
+
# Words are read one at a time: a line stops long before the end of its paragraph.
|
|
103
|
+
scanner = StringScanner.new(fragment)
|
|
104
|
+
while (segment = scanner.scan(breaks.scan_pattern))
|
|
105
|
+
segment_width = segment == breaks.zero_width_space ? 0 : @document.width_of(segment, kerning: @kerning)
|
|
106
|
+
if @accumulated_width + segment_width <= @width
|
|
107
|
+
@accumulated_width += segment_width
|
|
108
|
+
@accumulated_width -= @document.width_of(breaks.soft_hyphen, kerning: @kerning) if segment[-1] == breaks.soft_hyphen
|
|
109
|
+
@fragment_output << segment
|
|
110
|
+
else
|
|
111
|
+
@line_contains_more_than_one_word = false if @accumulated_width.zero? && @line_contains_more_than_one_word
|
|
112
|
+
end_of_the_line_reached(segment)
|
|
113
|
+
fragment_finished(fragment)
|
|
114
|
+
return false
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
fragment_finished(fragment)
|
|
118
|
+
true
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def line_empty?
|
|
122
|
+
@line_empty && @accumulated_width.zero?
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def initialize_line(options)
|
|
126
|
+
@document = options[:document]
|
|
127
|
+
@kerning = options[:kerning]
|
|
128
|
+
@width = options[:width]
|
|
129
|
+
@disable_wrap_by_char = options[:disable_wrap_by_char]
|
|
130
|
+
@accumulated_width = 0
|
|
131
|
+
@line_empty = true
|
|
132
|
+
@line_contains_more_than_one_word = false
|
|
133
|
+
@arranger = options[:arranger]
|
|
134
|
+
@arranger.initialize_line
|
|
135
|
+
@newline_encountered = false
|
|
136
|
+
@line_full = false
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def fragment_finished(fragment)
|
|
140
|
+
if fragment == "\n"
|
|
141
|
+
@newline_encountered = true
|
|
142
|
+
@line_empty = false
|
|
143
|
+
else
|
|
144
|
+
update_output_based_on_last_fragment(fragment, BREAKS_BY_ENCODING[fragment.encoding].soft_hyphen)
|
|
145
|
+
update_line_status_based_on_last_output
|
|
146
|
+
pull_preceding_fragment_to_join_this_one?(fragment)
|
|
147
|
+
end
|
|
148
|
+
remember_this_fragment_for_backward_looking_ops
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def update_output_based_on_last_fragment(fragment, normalized_soft_hyphen = nil)
|
|
152
|
+
remaining_text = fragment.slice(@fragment_output.length..fragment.length)
|
|
153
|
+
raise Errors::CannotFit if line_finished? && line_empty? && @fragment_output.empty? && !fragment.strip.empty?
|
|
154
|
+
|
|
155
|
+
@arranger.update_last_string(@fragment_output, remaining_text, normalized_soft_hyphen)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def pull_preceding_fragment_to_join_this_one?(current_fragment)
|
|
159
|
+
return unless @fragment_output.empty? && !current_fragment.empty? && @line_contains_more_than_one_word
|
|
160
|
+
return if @previous_fragment_ended_with_breakable || fragment_begins_with_breakable?(current_fragment)
|
|
161
|
+
|
|
162
|
+
@fragment_output = @previous_fragment_output_without_last_word
|
|
163
|
+
update_output_based_on_last_fragment(@previous_fragment)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def remember_this_fragment_for_backward_looking_ops
|
|
167
|
+
@previous_fragment = @fragment_output.dup
|
|
168
|
+
breaks = BREAKS_BY_ENCODING[@previous_fragment.encoding]
|
|
169
|
+
@previous_fragment_ended_with_breakable = @previous_fragment.match?(breaks.ends_with_break_pattern)
|
|
170
|
+
last_word = @previous_fragment.slice(breaks.last_word_pattern)
|
|
171
|
+
@previous_fragment_output_without_last_word = @previous_fragment.slice(0, @previous_fragment.length - (last_word&.length || 0))
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def fragment_begins_with_breakable?(fragment)
|
|
175
|
+
fragment.match?(BREAKS_BY_ENCODING[fragment.encoding].begins_with_break_pattern)
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def line_finished?
|
|
179
|
+
@line_full || paragraph_finished?
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def update_line_status_based_on_last_output
|
|
183
|
+
@line_contains_more_than_one_word = true if @fragment_output&.match?(BREAKS_BY_ENCODING[@fragment_output.encoding].word_division_pattern)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def end_of_the_line_reached(segment)
|
|
187
|
+
update_line_status_based_on_last_output
|
|
188
|
+
wrap_by_char(segment) unless @disable_wrap_by_char || @line_contains_more_than_one_word
|
|
189
|
+
@line_full = true
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def wrap_by_char(segment)
|
|
193
|
+
segment.each_char do |character|
|
|
194
|
+
character_width = @document.width_of(character)
|
|
195
|
+
break unless @accumulated_width + character_width <= @width
|
|
196
|
+
|
|
197
|
+
@accumulated_width += character_width
|
|
198
|
+
@fragment_output << character
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module Text
|
|
5
|
+
module Formatted
|
|
6
|
+
# Turns inline markup (<b>, <i>, <u>, <strikethrough>, <sub>, <sup>, <link>, <a>, <color>, <font>, <br>)
|
|
7
|
+
# into formatted text arrays, and back.
|
|
8
|
+
class Parser
|
|
9
|
+
PARSER_REGEX = Regexp.new(
|
|
10
|
+
"\n|<b>|</b>|<i>|</i>|<u>|</u>|<strikethrough>|</strikethrough>|<sub>|</sub>|<sup>|</sup>|<link[^>]*>|</link>|" \
|
|
11
|
+
"<color[^>]*>|</color>|<font[^>]*>|</font>|<strong>|</strong>|<em>|</em>|<a[^>]*>|</a>|[^<\n]+",
|
|
12
|
+
Regexp::MULTILINE,
|
|
13
|
+
)
|
|
14
|
+
ESCAPE_CHARS = {"&" => "&", ">" => ">", "<" => "<"}.freeze
|
|
15
|
+
UNESCAPE_CHARS = ESCAPE_CHARS.invert.freeze
|
|
16
|
+
ESCAPE_PATTERN = Regexp.union(ESCAPE_CHARS.keys)
|
|
17
|
+
UNESCAPE_PATTERN = Regexp.union(UNESCAPE_CHARS.keys)
|
|
18
|
+
STYLE_BY_OPENING_TAG = {
|
|
19
|
+
"<b>" => :bold, "<strong>" => :bold, "<i>" => :italic, "<em>" => :italic, "<u>" => :underline,
|
|
20
|
+
"<strikethrough>" => :strikethrough, "<sub>" => :subscript, "<sup>" => :superscript,
|
|
21
|
+
}.freeze
|
|
22
|
+
STYLE_BY_CLOSING_TAG = STYLE_BY_OPENING_TAG.transform_keys { it.sub("<", "</") }.freeze
|
|
23
|
+
PREFIX_BY_STYLE = {bold: "<b>", italic: "<i>", underline: "<u>", strikethrough: "<strikethrough>", subscript: "<sub>", superscript: "<sup>"}.freeze
|
|
24
|
+
SUFFIX_BY_STYLE = PREFIX_BY_STYLE.transform_values { it.sub("<", "</") }.freeze
|
|
25
|
+
|
|
26
|
+
def self.format(string, *_arguments)
|
|
27
|
+
array_from_tokens(string.gsub(%r{<br\s*/?>}, "\n").scan(PARSER_REGEX))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.to_string(array)
|
|
31
|
+
array.map do |hash|
|
|
32
|
+
prefix = +""
|
|
33
|
+
suffix = +""
|
|
34
|
+
hash[:styles]&.each do |style|
|
|
35
|
+
prefix << PREFIX_BY_STYLE[style]
|
|
36
|
+
suffix = SUFFIX_BY_STYLE[style] + suffix
|
|
37
|
+
end
|
|
38
|
+
font = (" name='#{hash[:font]}'" if hash[:font])
|
|
39
|
+
size = (" size='#{hash[:size]}'" if hash[:size])
|
|
40
|
+
character_spacing = (" character_spacing='#{hash[:character_spacing]}'" if hash[:character_spacing])
|
|
41
|
+
if font || size || character_spacing
|
|
42
|
+
prefix << "<font#{font}#{size}#{character_spacing}>"
|
|
43
|
+
suffix = "</font>"
|
|
44
|
+
end
|
|
45
|
+
link = (" href='#{hash[:link]}'" if hash[:link])
|
|
46
|
+
anchor = (" anchor='#{hash[:anchor]}'" if hash[:anchor])
|
|
47
|
+
if link || anchor
|
|
48
|
+
prefix << "<link#{link}#{anchor}>"
|
|
49
|
+
suffix = "</link>"
|
|
50
|
+
end
|
|
51
|
+
if hash[:color]
|
|
52
|
+
prefix <<
|
|
53
|
+
if hash[:color].is_a?(Array)
|
|
54
|
+
c, m, y, k = hash[:color]
|
|
55
|
+
"<color c='#{c}' m='#{m}' y='#{y}' k='#{k}'>"
|
|
56
|
+
else
|
|
57
|
+
"<color rgb='#{hash[:color]}'>"
|
|
58
|
+
end
|
|
59
|
+
suffix = "</color>"
|
|
60
|
+
end
|
|
61
|
+
prefix + escape(hash[:text]) + suffix
|
|
62
|
+
end.join
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def self.array_paragraphs(array)
|
|
66
|
+
paragraphs = []
|
|
67
|
+
paragraph = []
|
|
68
|
+
previous_string = "\n"
|
|
69
|
+
array.each do |hash|
|
|
70
|
+
hash[:text].scan(/[^\n]+|\n/).each do |string|
|
|
71
|
+
if string == "\n"
|
|
72
|
+
paragraph << hash.merge(text: "\n") if previous_string == "\n"
|
|
73
|
+
paragraphs << paragraph unless paragraph.empty?
|
|
74
|
+
paragraph = []
|
|
75
|
+
else
|
|
76
|
+
paragraph << hash.merge(text: string)
|
|
77
|
+
end
|
|
78
|
+
previous_string = string
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
paragraphs << paragraph unless paragraph.empty?
|
|
82
|
+
paragraphs
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def self.array_from_tokens(tokens)
|
|
86
|
+
array = []
|
|
87
|
+
styles = []
|
|
88
|
+
colors = []
|
|
89
|
+
link = nil
|
|
90
|
+
anchor = nil
|
|
91
|
+
local = nil
|
|
92
|
+
fonts = []
|
|
93
|
+
sizes = []
|
|
94
|
+
character_spacings = []
|
|
95
|
+
tokens.each do |token|
|
|
96
|
+
if (style = STYLE_BY_OPENING_TAG[token])
|
|
97
|
+
styles << style
|
|
98
|
+
elsif (style = STYLE_BY_CLOSING_TAG[token])
|
|
99
|
+
styles.delete(style)
|
|
100
|
+
else
|
|
101
|
+
case token
|
|
102
|
+
when "</link>", "</a>"
|
|
103
|
+
link = anchor = local = nil
|
|
104
|
+
when "</color>"
|
|
105
|
+
colors.pop
|
|
106
|
+
when "</font>"
|
|
107
|
+
fonts.pop
|
|
108
|
+
sizes.pop
|
|
109
|
+
character_spacings.pop
|
|
110
|
+
when /^<link[^>]*>$/, /^<a[^>]*>$/
|
|
111
|
+
link = attribute(token, "href") || link
|
|
112
|
+
anchor = attribute(token, "anchor") || anchor
|
|
113
|
+
local = attribute(token, "local") || local
|
|
114
|
+
when /^<color[^>]*>$/
|
|
115
|
+
rgb = attribute(token, "rgb", hash_prefixed: true)
|
|
116
|
+
colors << rgb if rgb
|
|
117
|
+
cmyk = %w[c m y k].map { attribute(token, it, hash_prefixed: true)&.then { |value| Integer(value, 10) } }
|
|
118
|
+
colors << cmyk if cmyk.all?
|
|
119
|
+
when /^<font[^>]*>$/
|
|
120
|
+
attribute(token, "name")&.then { fonts << it }
|
|
121
|
+
attribute(token, "size")&.then { sizes << Float(it) }
|
|
122
|
+
attribute(token, "character_spacing")&.then { character_spacings << Float(it) }
|
|
123
|
+
else
|
|
124
|
+
array << {
|
|
125
|
+
text: unescape(token), styles: styles.dup, color: colors.last, local:, link:, anchor:,
|
|
126
|
+
font: fonts.last, size: sizes.last, character_spacing: character_spacings.last,
|
|
127
|
+
}
|
|
128
|
+
end
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
array
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def self.attribute(token, name, hash_prefixed: false)
|
|
135
|
+
prefix = hash_prefixed ? "#?" : ""
|
|
136
|
+
token[/\b#{name}="#{prefix}([^"]*)"/, 1] || token[/\b#{name}='#{prefix}([^']*)'/, 1]
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def self.escape(text)
|
|
140
|
+
text.gsub(ESCAPE_PATTERN, ESCAPE_CHARS)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def self.unescape(text)
|
|
144
|
+
text.gsub(UNESCAPE_PATTERN, UNESCAPE_CHARS)
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module Text
|
|
5
|
+
module Formatted
|
|
6
|
+
# Lays formatted text out line by line inside a box's width and height.
|
|
7
|
+
module Wrap
|
|
8
|
+
def initialize(_array, options)
|
|
9
|
+
@line_wrap = LineWrap.new
|
|
10
|
+
@arranger = Arranger.new(@document, kerning: options[:kerning])
|
|
11
|
+
@disable_wrap_by_char = options[:disable_wrap_by_char]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def wrap(array)
|
|
15
|
+
initialize_wrap(array)
|
|
16
|
+
loop do
|
|
17
|
+
@line_wrap.wrap_line(document: @document, kerning: @kerning, width: available_width, arranger: @arranger,
|
|
18
|
+
disable_wrap_by_char: @disable_wrap_by_char)
|
|
19
|
+
break unless enough_height_for_this_line?
|
|
20
|
+
|
|
21
|
+
move_baseline_down
|
|
22
|
+
print_line
|
|
23
|
+
break if @single_line || @arranger.finished?
|
|
24
|
+
end
|
|
25
|
+
@text = @printed_lines.join("\n")
|
|
26
|
+
@everything_printed = @arranger.finished?
|
|
27
|
+
@arranger.unconsumed
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def print_line
|
|
33
|
+
@nothing_printed = false
|
|
34
|
+
printed_fragments = []
|
|
35
|
+
fragments_this_line = []
|
|
36
|
+
word_spacing = word_spacing_for_this_line
|
|
37
|
+
@arranger.fragments.each do |fragment|
|
|
38
|
+
fragment.word_spacing = word_spacing
|
|
39
|
+
if fragment.text == "\n"
|
|
40
|
+
printed_fragments << "\n" if @printed_lines.last == ""
|
|
41
|
+
break
|
|
42
|
+
end
|
|
43
|
+
printed_fragments << fragment.text
|
|
44
|
+
fragments_this_line << fragment
|
|
45
|
+
end
|
|
46
|
+
@arranger.fragments.replace([])
|
|
47
|
+
|
|
48
|
+
accumulated_width = 0
|
|
49
|
+
drawn_fragments = []
|
|
50
|
+
fragments_this_line.reverse! if @direction == :rtl
|
|
51
|
+
fragments_this_line.each do |fragment|
|
|
52
|
+
fragment.default_direction = @direction
|
|
53
|
+
drawn_fragment = [fragment, accumulated_width, @line_wrap.width, word_spacing]
|
|
54
|
+
format_and_draw_fragment(*drawn_fragment)
|
|
55
|
+
drawn_fragments << drawn_fragment if @laid_out_lines
|
|
56
|
+
accumulated_width += fragment.width
|
|
57
|
+
end
|
|
58
|
+
@laid_out_lines << [@baseline_y, drawn_fragments] if @laid_out_lines
|
|
59
|
+
@printed_lines << printed_fragments.map { it.dup.force_encoding(::Encoding::UTF_8) }.join
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def word_spacing_for_this_line
|
|
63
|
+
return 0 unless @align == :justify && @line_wrap.space_count.positive? && !@line_wrap.paragraph_finished?
|
|
64
|
+
|
|
65
|
+
(available_width - @line_wrap.width) / @line_wrap.space_count
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Slack in points, so floating point sums do not drop a line that fits exactly.
|
|
69
|
+
HEIGHT_TOLERANCE = 0.0001
|
|
70
|
+
|
|
71
|
+
def enough_height_for_this_line?
|
|
72
|
+
@line_height = @arranger.max_line_height
|
|
73
|
+
@descender = @arranger.max_descender
|
|
74
|
+
@ascender = @arranger.max_ascender
|
|
75
|
+
line_extent = @baseline_y.zero? ? @ascender + @descender : @descender + @line_height + @leading
|
|
76
|
+
return true if @baseline_y.abs + line_extent <= @height + HEIGHT_TOLERANCE
|
|
77
|
+
|
|
78
|
+
@arranger.repack_unretrieved
|
|
79
|
+
false
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def initialize_wrap(array)
|
|
83
|
+
@text = nil
|
|
84
|
+
@arranger.format_array = array
|
|
85
|
+
@line_height = 0
|
|
86
|
+
@descender = 0
|
|
87
|
+
@ascender = 0
|
|
88
|
+
@baseline_y = 0
|
|
89
|
+
@printed_lines = []
|
|
90
|
+
@laid_out_lines&.clear
|
|
91
|
+
@nothing_printed = true
|
|
92
|
+
@everything_printed = false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def format_and_draw_fragment(fragment, accumulated_width, line_width, word_spacing)
|
|
96
|
+
@arranger.apply_color_and_font_settings(fragment) do
|
|
97
|
+
draw_fragment(fragment, accumulated_width, line_width, word_spacing)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|