gamba 2.0.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 +7 -0
- data/CHANGELOG.md +80 -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 +148 -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/gamba/annotations.rb +21 -0
- data/lib/gamba/combine.rb +307 -0
- data/lib/gamba/document/bounding_box.rb +193 -0
- data/lib/gamba/document/column_box.rb +91 -0
- data/lib/gamba/document/internals.rb +264 -0
- data/lib/gamba/document/span.rb +25 -0
- data/lib/gamba/document.rb +268 -0
- data/lib/gamba/encoding.rb +13 -0
- data/lib/gamba/errors.rb +39 -0
- data/lib/gamba/font.rb +228 -0
- data/lib/gamba/fonts/afm.rb +171 -0
- data/lib/gamba/fonts/otf.rb +50 -0
- data/lib/gamba/fonts/ttf.rb +413 -0
- data/lib/gamba/graphic_state.rb +84 -0
- data/lib/gamba/graphics/color.rb +174 -0
- data/lib/gamba/graphics/patterns.rb +100 -0
- data/lib/gamba/graphics/style.rb +159 -0
- data/lib/gamba/graphics.rb +212 -0
- data/lib/gamba/grid.rb +216 -0
- data/lib/gamba/images/image.rb +271 -0
- data/lib/gamba/images/png_decoder.rb +65 -0
- data/lib/gamba/images.rb +115 -0
- data/lib/gamba/measurement_extensions.rb +40 -0
- data/lib/gamba/measurements.rb +62 -0
- data/lib/gamba/outline.rb +148 -0
- data/lib/gamba/page.rb +125 -0
- data/lib/gamba/page_geometry.rb +27 -0
- data/lib/gamba/pdf/decryption.rb +165 -0
- data/lib/gamba/pdf/outline_node.rb +53 -0
- data/lib/gamba/pdf/reader.rb +616 -0
- data/lib/gamba/pdf/reference.rb +31 -0
- data/lib/gamba/pdf/writer.rb +105 -0
- data/lib/gamba/pdf.rb +87 -0
- data/lib/gamba/pdf_a/srgb_profile.rb +61 -0
- data/lib/gamba/pdf_a.rb +132 -0
- data/lib/gamba/repeater.rb +79 -0
- data/lib/gamba/security.rb +111 -0
- data/lib/gamba/stamp.rb +76 -0
- data/lib/gamba/table/cell/kinds.rb +246 -0
- data/lib/gamba/table/cell.rb +331 -0
- data/lib/gamba/table/cells.rb +208 -0
- data/lib/gamba/table.rb +298 -0
- data/lib/gamba/text/box.rb +16 -0
- data/lib/gamba/text/formatted/arranger.rb +217 -0
- data/lib/gamba/text/formatted/box.rb +312 -0
- data/lib/gamba/text/formatted/fragment.rb +205 -0
- data/lib/gamba/text/formatted/line_wrap.rb +205 -0
- data/lib/gamba/text/formatted/parser.rb +150 -0
- data/lib/gamba/text/formatted/wrap.rb +103 -0
- data/lib/gamba/text.rb +310 -0
- data/lib/gamba/transformation_stack.rb +43 -0
- data/lib/gamba/true_type/cff_subset.rb +157 -0
- data/lib/gamba/true_type/file.rb +372 -0
- data/lib/gamba/true_type/open_type_layout.rb +189 -0
- data/lib/gamba/true_type/simple_subsets.rb +108 -0
- data/lib/gamba/true_type/subset.rb +206 -0
- data/lib/gamba/version.rb +10 -0
- data/lib/gamba/view.rb +28 -0
- data/lib/gamba.rb +124 -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 +180 -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 +169 -0
- data/lib/prawn/svg/elements/anchor.rb +10 -0
- data/lib/prawn/svg/elements/base.rb +315 -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 +19 -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 +28 -0
- data/lib/prawn/svg/elements/ellipse.rb +20 -0
- data/lib/prawn/svg/elements/gradient.rb +183 -0
- data/lib/prawn/svg/elements/ignored.rb +5 -0
- data/lib/prawn/svg/elements/image.rb +178 -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 +352 -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 +39 -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 +52 -0
- data/lib/prawn.rb +3 -0
- metadata +263 -0
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
module Text
|
|
5
|
+
module Formatted
|
|
6
|
+
# A run of text sharing one format on one line, with its measurements and, once drawn, its position.
|
|
7
|
+
class Fragment
|
|
8
|
+
attr_reader :format_state, :text
|
|
9
|
+
attr_writer :width
|
|
10
|
+
attr_accessor :line_height, :descender, :ascender, :word_spacing, :left, :baseline
|
|
11
|
+
|
|
12
|
+
def initialize(text, format_state, document)
|
|
13
|
+
@format_state = format_state
|
|
14
|
+
@document = document
|
|
15
|
+
@word_spacing = 0
|
|
16
|
+
@original_text = text
|
|
17
|
+
@text = process_text(@original_text)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def width
|
|
21
|
+
@word_spacing.zero? ? @width : @width + (@word_spacing * space_count)
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def height
|
|
25
|
+
top - bottom
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def subscript?
|
|
29
|
+
styles.include?(:subscript)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def superscript?
|
|
33
|
+
styles.include?(:superscript)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def y_offset
|
|
37
|
+
if subscript? then -descender
|
|
38
|
+
elsif superscript? then 0.85 * ascender
|
|
39
|
+
else 0
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def bounding_box
|
|
44
|
+
[left, bottom, right, top]
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def absolute_bounding_box
|
|
48
|
+
left, bottom, right, top = bounding_box
|
|
49
|
+
bounds = @document.bounds
|
|
50
|
+
[left + bounds.absolute_left, bottom + bounds.absolute_bottom, right + bounds.absolute_left, top + bounds.absolute_bottom]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def underline_points
|
|
54
|
+
y = baseline - 1.25
|
|
55
|
+
[[left, y], [right, y]]
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def strikethrough_points
|
|
59
|
+
y = baseline + (ascender * 0.3)
|
|
60
|
+
[[left, y], [right, y]]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def styles
|
|
64
|
+
@format_state[:styles] || []
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def link
|
|
68
|
+
@format_state[:link]
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def anchor
|
|
72
|
+
@format_state[:anchor]
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def local
|
|
76
|
+
@format_state[:local]
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def color
|
|
80
|
+
@format_state[:color]
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def font
|
|
84
|
+
@format_state[:font]
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def size
|
|
88
|
+
@format_state[:size]
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def character_spacing
|
|
92
|
+
@format_state[:character_spacing] || @document.character_spacing
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def direction
|
|
96
|
+
@format_state[:direction]
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def default_direction=(direction)
|
|
100
|
+
return if @format_state[:direction]
|
|
101
|
+
|
|
102
|
+
@format_state[:direction] = direction
|
|
103
|
+
@text = process_text(@original_text)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def include_trailing_white_space!
|
|
107
|
+
@format_state.delete(:exclude_trailing_white_space)
|
|
108
|
+
@text = process_text(@original_text)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def space_count
|
|
112
|
+
@text.count(" ")
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def callback_objects
|
|
116
|
+
callback = @format_state[:callback]
|
|
117
|
+
callback.nil? ? [] : Array(callback)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def right
|
|
121
|
+
left + width
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def top
|
|
125
|
+
baseline + ascender
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def bottom
|
|
129
|
+
baseline - descender
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def top_left
|
|
133
|
+
[left, top]
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def top_right
|
|
137
|
+
[right, top]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def bottom_right
|
|
141
|
+
[right, bottom]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def bottom_left
|
|
145
|
+
[left, bottom]
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def absolute_left
|
|
149
|
+
absolute_bounding_box[0]
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def absolute_right
|
|
153
|
+
absolute_bounding_box[2]
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def absolute_top
|
|
157
|
+
absolute_bounding_box[3]
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def absolute_bottom
|
|
161
|
+
absolute_bounding_box[1]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def absolute_top_left
|
|
165
|
+
[absolute_left, absolute_top]
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def absolute_top_right
|
|
169
|
+
[absolute_right, absolute_top]
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def absolute_bottom_left
|
|
173
|
+
[absolute_left, absolute_bottom]
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def absolute_bottom_right
|
|
177
|
+
[absolute_right, absolute_bottom]
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
private
|
|
181
|
+
|
|
182
|
+
def process_text(text)
|
|
183
|
+
string = (text.encoding == ::Encoding::UTF_8) ? text.gsub(ZWSP, "") : text
|
|
184
|
+
if @format_state[:exclude_trailing_white_space]
|
|
185
|
+
string = string.rstrip
|
|
186
|
+
string = process_soft_hyphens(string[0..-2]) + string[-1..] if soft_hyphens_need_processing?(string)
|
|
187
|
+
elsif soft_hyphens_need_processing?(string)
|
|
188
|
+
string = process_soft_hyphens(string)
|
|
189
|
+
end
|
|
190
|
+
(direction == :rtl) ? string.reverse : string
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def soft_hyphens_need_processing?(string)
|
|
194
|
+
!string.empty? && @format_state[:normalized_soft_hyphen]
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def process_soft_hyphens(string)
|
|
198
|
+
soft_hyphen = @format_state[:normalized_soft_hyphen]
|
|
199
|
+
string = string.dup.force_encoding(soft_hyphen.encoding) if string.encoding != soft_hyphen.encoding
|
|
200
|
+
string.gsub(soft_hyphen, "")
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "strscan"
|
|
4
|
+
|
|
5
|
+
module Prawn
|
|
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 { _1.encode(encoding) }.join("|")),
|
|
36
|
+
word_division_pattern: Regexp.union([*["\t", "\n", "\v", "\r", " "].map { _1.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, @width_options)
|
|
106
|
+
if @accumulated_width + segment_width <= @width
|
|
107
|
+
@accumulated_width += segment_width
|
|
108
|
+
@accumulated_width -= @document.width_of(breaks.soft_hyphen, @width_options) if breaks.soft_hyphen && segment.end_with?(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
|
+
# Shared by every word measured on the line: passing kerning: to each call built a hash per word.
|
|
128
|
+
@width_options = {kerning: options[:kerning]}
|
|
129
|
+
@width = options[:width]
|
|
130
|
+
@disable_wrap_by_char = options[:disable_wrap_by_char]
|
|
131
|
+
@accumulated_width = 0
|
|
132
|
+
@line_empty = true
|
|
133
|
+
@line_contains_more_than_one_word = false
|
|
134
|
+
@arranger = options[:arranger]
|
|
135
|
+
@arranger.initialize_line
|
|
136
|
+
@newline_encountered = false
|
|
137
|
+
@line_full = false
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def fragment_finished(fragment)
|
|
141
|
+
if fragment == "\n"
|
|
142
|
+
@newline_encountered = true
|
|
143
|
+
@line_empty = false
|
|
144
|
+
else
|
|
145
|
+
update_output_based_on_last_fragment(fragment, BREAKS_BY_ENCODING[fragment.encoding].soft_hyphen)
|
|
146
|
+
update_line_status_based_on_last_output
|
|
147
|
+
pull_preceding_fragment_to_join_this_one?(fragment)
|
|
148
|
+
end
|
|
149
|
+
remember_this_fragment_for_backward_looking_ops
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def update_output_based_on_last_fragment(fragment, normalized_soft_hyphen = nil)
|
|
153
|
+
remaining_text = fragment.slice(@fragment_output.length..fragment.length)
|
|
154
|
+
raise Errors::CannotFit if line_finished? && line_empty? && @fragment_output.empty? && !fragment.strip.empty?
|
|
155
|
+
|
|
156
|
+
@arranger.update_last_string(@fragment_output, remaining_text, normalized_soft_hyphen)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
def pull_preceding_fragment_to_join_this_one?(current_fragment)
|
|
160
|
+
return unless @fragment_output.empty? && !current_fragment.empty? && @line_contains_more_than_one_word
|
|
161
|
+
return if @previous_fragment_ended_with_breakable || fragment_begins_with_breakable?(current_fragment)
|
|
162
|
+
|
|
163
|
+
@fragment_output = @previous_fragment_output_without_last_word
|
|
164
|
+
update_output_based_on_last_fragment(@previous_fragment)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def remember_this_fragment_for_backward_looking_ops
|
|
168
|
+
@previous_fragment = @fragment_output.dup
|
|
169
|
+
breaks = BREAKS_BY_ENCODING[@previous_fragment.encoding]
|
|
170
|
+
@previous_fragment_ended_with_breakable = @previous_fragment.match?(breaks.ends_with_break_pattern)
|
|
171
|
+
last_word = @previous_fragment.slice(breaks.last_word_pattern)
|
|
172
|
+
@previous_fragment_output_without_last_word = @previous_fragment.slice(0, @previous_fragment.length - (last_word&.length || 0))
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def fragment_begins_with_breakable?(fragment)
|
|
176
|
+
fragment.match?(BREAKS_BY_ENCODING[fragment.encoding].begins_with_break_pattern)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
def line_finished?
|
|
180
|
+
@line_full || paragraph_finished?
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
def update_line_status_based_on_last_output
|
|
184
|
+
@line_contains_more_than_one_word = true if @fragment_output&.match?(BREAKS_BY_ENCODING[@fragment_output.encoding].word_division_pattern)
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def end_of_the_line_reached(segment)
|
|
188
|
+
update_line_status_based_on_last_output
|
|
189
|
+
wrap_by_char(segment) unless @disable_wrap_by_char || @line_contains_more_than_one_word
|
|
190
|
+
@line_full = true
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def wrap_by_char(segment)
|
|
194
|
+
segment.each_char do |character|
|
|
195
|
+
character_width = @document.width_of(character)
|
|
196
|
+
break unless @accumulated_width + character_width <= @width
|
|
197
|
+
|
|
198
|
+
@accumulated_width += character_width
|
|
199
|
+
@fragment_output << character
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
end
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
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
|
+
CMYK_ATTRIBUTES = %w[c m y k].freeze
|
|
17
|
+
ESCAPE_PATTERN = Regexp.union(ESCAPE_CHARS.keys)
|
|
18
|
+
UNESCAPE_PATTERN = Regexp.union(UNESCAPE_CHARS.keys)
|
|
19
|
+
STYLE_BY_OPENING_TAG = {
|
|
20
|
+
"<b>" => :bold, "<strong>" => :bold, "<i>" => :italic, "<em>" => :italic, "<u>" => :underline,
|
|
21
|
+
"<strikethrough>" => :strikethrough, "<sub>" => :subscript, "<sup>" => :superscript
|
|
22
|
+
}.freeze
|
|
23
|
+
STYLE_BY_CLOSING_TAG = STYLE_BY_OPENING_TAG.transform_keys { _1.sub("<", "</") }.freeze
|
|
24
|
+
PREFIX_BY_STYLE = {bold: "<b>", italic: "<i>", underline: "<u>", strikethrough: "<strikethrough>", subscript: "<sub>", superscript: "<sup>"}.freeze
|
|
25
|
+
SUFFIX_BY_STYLE = PREFIX_BY_STYLE.transform_values { _1.sub("<", "</") }.freeze
|
|
26
|
+
|
|
27
|
+
def self.format(string, *_arguments)
|
|
28
|
+
array_from_tokens(string.gsub(%r{<br\s*/?>}, "\n").scan(PARSER_REGEX))
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.to_string(array)
|
|
32
|
+
array.map do |hash|
|
|
33
|
+
prefix = +""
|
|
34
|
+
suffix = +""
|
|
35
|
+
hash[:styles]&.each do |style|
|
|
36
|
+
prefix << PREFIX_BY_STYLE[style]
|
|
37
|
+
suffix = SUFFIX_BY_STYLE[style] + suffix
|
|
38
|
+
end
|
|
39
|
+
font = (" name='#{hash[:font]}'" if hash[:font])
|
|
40
|
+
size = (" size='#{hash[:size]}'" if hash[:size])
|
|
41
|
+
character_spacing = (" character_spacing='#{hash[:character_spacing]}'" if hash[:character_spacing])
|
|
42
|
+
if font || size || character_spacing
|
|
43
|
+
prefix << "<font#{font}#{size}#{character_spacing}>"
|
|
44
|
+
suffix = "</font>"
|
|
45
|
+
end
|
|
46
|
+
link = (" href='#{hash[:link]}'" if hash[:link])
|
|
47
|
+
anchor = (" anchor='#{hash[:anchor]}'" if hash[:anchor])
|
|
48
|
+
if link || anchor
|
|
49
|
+
prefix << "<link#{link}#{anchor}>"
|
|
50
|
+
suffix = "</link>"
|
|
51
|
+
end
|
|
52
|
+
if hash[:color]
|
|
53
|
+
prefix <<
|
|
54
|
+
if hash[:color].is_a?(Array)
|
|
55
|
+
c, m, y, k = hash[:color]
|
|
56
|
+
"<color c='#{c}' m='#{m}' y='#{y}' k='#{k}'>"
|
|
57
|
+
else
|
|
58
|
+
"<color rgb='#{hash[:color]}'>"
|
|
59
|
+
end
|
|
60
|
+
suffix = "</color>"
|
|
61
|
+
end
|
|
62
|
+
prefix + escape(hash[:text]) + suffix
|
|
63
|
+
end.join
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.array_paragraphs(array)
|
|
67
|
+
paragraphs = []
|
|
68
|
+
paragraph = []
|
|
69
|
+
previous_string = "\n"
|
|
70
|
+
array.each do |hash|
|
|
71
|
+
hash[:text].scan(/[^\n]+|\n/).each do |string|
|
|
72
|
+
if string == "\n"
|
|
73
|
+
paragraph << hash.merge(text: "\n") if previous_string == "\n"
|
|
74
|
+
paragraphs << paragraph unless paragraph.empty?
|
|
75
|
+
paragraph = []
|
|
76
|
+
else
|
|
77
|
+
paragraph << hash.merge(text: string)
|
|
78
|
+
end
|
|
79
|
+
previous_string = string
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
paragraphs << paragraph unless paragraph.empty?
|
|
83
|
+
paragraphs
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def self.array_from_tokens(tokens)
|
|
87
|
+
array = []
|
|
88
|
+
styles = []
|
|
89
|
+
colors = []
|
|
90
|
+
link = nil
|
|
91
|
+
anchor = nil
|
|
92
|
+
local = nil
|
|
93
|
+
fonts = []
|
|
94
|
+
sizes = []
|
|
95
|
+
character_spacings = []
|
|
96
|
+
tokens.each do |token|
|
|
97
|
+
if (style = STYLE_BY_OPENING_TAG[token])
|
|
98
|
+
styles << style
|
|
99
|
+
elsif (style = STYLE_BY_CLOSING_TAG[token])
|
|
100
|
+
styles.delete(style)
|
|
101
|
+
else
|
|
102
|
+
case token
|
|
103
|
+
when "</link>", "</a>"
|
|
104
|
+
link = anchor = local = nil
|
|
105
|
+
when "</color>"
|
|
106
|
+
colors.pop
|
|
107
|
+
when "</font>"
|
|
108
|
+
fonts.pop
|
|
109
|
+
sizes.pop
|
|
110
|
+
character_spacings.pop
|
|
111
|
+
when /^<link[^>]*>$/, /^<a[^>]*>$/
|
|
112
|
+
link = attribute(token, "href") || link
|
|
113
|
+
anchor = attribute(token, "anchor") || anchor
|
|
114
|
+
local = attribute(token, "local") || local
|
|
115
|
+
when /^<color[^>]*>$/
|
|
116
|
+
rgb = attribute(token, "rgb", hash_prefixed: true)
|
|
117
|
+
colors << rgb if rgb
|
|
118
|
+
cmyk = CMYK_ATTRIBUTES.map { |name| attribute(token, name, hash_prefixed: true)&.then { |value| Integer(value, 10) } }
|
|
119
|
+
colors << cmyk if cmyk.all?
|
|
120
|
+
when /^<font[^>]*>$/
|
|
121
|
+
attribute(token, "name")&.then { fonts << _1 }
|
|
122
|
+
attribute(token, "size")&.then { sizes << Float(_1) }
|
|
123
|
+
attribute(token, "character_spacing")&.then { character_spacings << Float(_1) }
|
|
124
|
+
else
|
|
125
|
+
array << {
|
|
126
|
+
text: unescape(token), styles: styles.dup, color: colors.last, local:, link:, anchor:,
|
|
127
|
+
font: fonts.last, size: sizes.last, character_spacing: character_spacings.last
|
|
128
|
+
}
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
array
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def self.attribute(token, name, hash_prefixed: false)
|
|
136
|
+
prefix = hash_prefixed ? "#?" : ""
|
|
137
|
+
token[/\b#{name}="#{prefix}([^"]*)"/, 1] || token[/\b#{name}='#{prefix}([^']*)'/, 1]
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def self.escape(text)
|
|
141
|
+
text.gsub(ESCAPE_PATTERN, ESCAPE_CHARS)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def self.unescape(text)
|
|
145
|
+
text.gsub(UNESCAPE_PATTERN, UNESCAPE_CHARS)
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
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 { _1.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
|