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,217 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
module Text
|
|
5
|
+
module Formatted
|
|
6
|
+
# Hands the line wrapper one string at a time out of formatted text, takes back what did not fit, and
|
|
7
|
+
# measures the fragments of each finished line.
|
|
8
|
+
class Arranger
|
|
9
|
+
class NotFinalized < StandardError
|
|
10
|
+
DEFAULT_MESSAGE = "Lines must be finalized"
|
|
11
|
+
|
|
12
|
+
def initialize(message = DEFAULT_MESSAGE, method: nil)
|
|
13
|
+
super((method && message == DEFAULT_MESSAGE) ? "Lines must be finalized before calling ##{method}" : message)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
class BadFontFamily < StandardError
|
|
18
|
+
def initialize(message = "Bad font family")
|
|
19
|
+
super
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Relative size of subscript and superscript text.
|
|
24
|
+
SCRIPT_SIZE_RATIO = 0.583
|
|
25
|
+
|
|
26
|
+
attr_reader :max_line_height, :max_descender, :max_ascender, :finalized, :unconsumed, :fragments, :current_format_state
|
|
27
|
+
attr_accessor :consumed
|
|
28
|
+
|
|
29
|
+
def initialize(document, options = {})
|
|
30
|
+
@document = document
|
|
31
|
+
@fragments = []
|
|
32
|
+
@unconsumed = []
|
|
33
|
+
@kerning = options[:kerning]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def space_count
|
|
37
|
+
raise NotFinalized.new(method: "space_count") unless finalized
|
|
38
|
+
|
|
39
|
+
@fragments.sum(&:space_count)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def line_width
|
|
43
|
+
raise NotFinalized.new(method: "line_width") unless finalized
|
|
44
|
+
|
|
45
|
+
@fragments.inject(0) { |sum, fragment| sum + fragment.width }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def line
|
|
49
|
+
raise NotFinalized.new(method: "line") unless finalized
|
|
50
|
+
|
|
51
|
+
@fragments.map do |fragment|
|
|
52
|
+
fragment.text.encode(::Encoding::UTF_8)
|
|
53
|
+
rescue ::Encoding::InvalidByteSequenceError, ::Encoding::UndefinedConversionError
|
|
54
|
+
fragment.text.dup.force_encoding(::Encoding::UTF_8)
|
|
55
|
+
end.join
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def finalize_line
|
|
59
|
+
@finalized = true
|
|
60
|
+
omit_trailing_whitespace_from_line_width
|
|
61
|
+
@fragments = []
|
|
62
|
+
@consumed.each do |hash|
|
|
63
|
+
fragment = Fragment.new(hash[:text], hash.except(:text), @document)
|
|
64
|
+
@fragments << fragment
|
|
65
|
+
measure(fragment)
|
|
66
|
+
@max_line_height = [@max_line_height, fragment.line_height].compact.max
|
|
67
|
+
@max_descender = [@max_descender, fragment.descender].compact.max
|
|
68
|
+
@max_ascender = [@max_ascender, fragment.ascender].compact.max
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def format_array=(array)
|
|
73
|
+
initialize_line
|
|
74
|
+
@unconsumed = []
|
|
75
|
+
array.each do |hash|
|
|
76
|
+
hash[:text].scan(/[^\n]+|\n/) { |line| @unconsumed << hash.merge(text: line) }
|
|
77
|
+
end
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def initialize_line
|
|
81
|
+
@finalized = false
|
|
82
|
+
@max_line_height = 0
|
|
83
|
+
@max_descender = 0
|
|
84
|
+
@max_ascender = 0
|
|
85
|
+
@consumed = []
|
|
86
|
+
@fragments = []
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def finished?
|
|
90
|
+
@unconsumed.empty?
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def next_string
|
|
94
|
+
raise NotFinalized.new(method: "next_string") if finalized
|
|
95
|
+
|
|
96
|
+
next_unconsumed_hash = @unconsumed.shift
|
|
97
|
+
return unless next_unconsumed_hash
|
|
98
|
+
|
|
99
|
+
@consumed << next_unconsumed_hash.dup
|
|
100
|
+
@current_format_state = next_unconsumed_hash.except(:text)
|
|
101
|
+
next_unconsumed_hash[:text]
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def preview_next_string
|
|
105
|
+
@unconsumed.first&.fetch(:text)
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def apply_color_and_font_settings(fragment, &)
|
|
109
|
+
return apply_font_settings(fragment, &) unless fragment.color
|
|
110
|
+
|
|
111
|
+
original_fill_color = @document.fill_color
|
|
112
|
+
original_stroke_color = @document.stroke_color
|
|
113
|
+
@document.fill_color(*fragment.color)
|
|
114
|
+
@document.stroke_color(*fragment.color)
|
|
115
|
+
apply_font_settings(fragment, &)
|
|
116
|
+
@document.stroke_color = original_stroke_color
|
|
117
|
+
@document.fill_color = original_fill_color
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def apply_font_settings(fragment = nil, &)
|
|
121
|
+
if fragment
|
|
122
|
+
font = fragment.font
|
|
123
|
+
size = fragment.size
|
|
124
|
+
character_spacing = fragment.character_spacing
|
|
125
|
+
styles = fragment.styles
|
|
126
|
+
else
|
|
127
|
+
font = current_format_state[:font]
|
|
128
|
+
size = current_format_state[:size]
|
|
129
|
+
character_spacing = current_format_state[:character_spacing] || @document.character_spacing
|
|
130
|
+
styles = current_format_state[:styles]
|
|
131
|
+
end
|
|
132
|
+
font_style = font_style(styles)
|
|
133
|
+
|
|
134
|
+
@document.character_spacing(character_spacing) do
|
|
135
|
+
if font || font_style != :normal
|
|
136
|
+
raise BadFontFamily unless @document.font.family
|
|
137
|
+
|
|
138
|
+
@document.font(font || @document.font.family, style: font_style) { apply_font_size(size, styles, &) }
|
|
139
|
+
else
|
|
140
|
+
apply_font_size(size, styles, &)
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def update_last_string(printed, unprinted, normalized_soft_hyphen = nil)
|
|
146
|
+
return if printed.nil?
|
|
147
|
+
|
|
148
|
+
if printed.empty?
|
|
149
|
+
@consumed.pop
|
|
150
|
+
else
|
|
151
|
+
@consumed.last[:text] = printed
|
|
152
|
+
@consumed.last[:normalized_soft_hyphen] = normalized_soft_hyphen if normalized_soft_hyphen
|
|
153
|
+
end
|
|
154
|
+
@unconsumed.unshift(@current_format_state.merge(text: unprinted)) unless unprinted.empty?
|
|
155
|
+
load_previous_format_state if printed.empty?
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def retrieve_fragment
|
|
159
|
+
raise NotFinalized, "Lines must be finalized before fragments can be retrieved" unless finalized
|
|
160
|
+
|
|
161
|
+
@fragments.shift
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def repack_unretrieved
|
|
165
|
+
new_unconsumed = []
|
|
166
|
+
while (fragment = retrieve_fragment)
|
|
167
|
+
fragment.include_trailing_white_space!
|
|
168
|
+
new_unconsumed << fragment.format_state.merge(text: fragment.text)
|
|
169
|
+
end
|
|
170
|
+
@unconsumed = new_unconsumed.concat(@unconsumed)
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def font_style(styles)
|
|
174
|
+
styles = Array(styles)
|
|
175
|
+
bold = styles.include?(:bold)
|
|
176
|
+
italic = styles.include?(:italic)
|
|
177
|
+
if bold && italic then :bold_italic
|
|
178
|
+
elsif bold then :bold
|
|
179
|
+
elsif italic then :italic
|
|
180
|
+
else :normal
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
private
|
|
185
|
+
|
|
186
|
+
def load_previous_format_state
|
|
187
|
+
@current_format_state = @consumed.empty? ? {} : @consumed.last.except(:text)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def apply_font_size(size, styles, &)
|
|
191
|
+
if styles&.include?(:subscript) || styles&.include?(:superscript)
|
|
192
|
+
size = (size || @document.font_size) * SCRIPT_SIZE_RATIO
|
|
193
|
+
end
|
|
194
|
+
size.nil? ? yield : @document.font_size(size, &)
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
def omit_trailing_whitespace_from_line_width
|
|
198
|
+
@consumed.reverse_each do |hash|
|
|
199
|
+
break if hash[:text] == "\n"
|
|
200
|
+
|
|
201
|
+
hash[:exclude_trailing_white_space] = true
|
|
202
|
+
break unless hash[:text].strip.empty? && @consumed.length > 1
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
def measure(fragment)
|
|
207
|
+
apply_font_settings(fragment) do
|
|
208
|
+
fragment.width = @document.width_of(fragment.text, kerning: @kerning)
|
|
209
|
+
fragment.line_height = @document.font.height
|
|
210
|
+
fragment.descender = @document.font.descender
|
|
211
|
+
fragment.ascender = @document.font.ascender
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
end
|
|
217
|
+
end
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
module Text
|
|
5
|
+
module Formatted
|
|
6
|
+
# Formatted text laid out in a rectangle: wrapped, aligned, shrunk or truncated to fit, possibly rotated.
|
|
7
|
+
class Box
|
|
8
|
+
include Wrap
|
|
9
|
+
|
|
10
|
+
VALID_OPTIONS = (Text::VALID_OPTIONS + %i[
|
|
11
|
+
at height width align valign rotate rotate_around overflow min_font_size disable_wrap_by_char leading
|
|
12
|
+
character_spacing mode single_line document direction fallback_fonts draw_text_callback
|
|
13
|
+
]).freeze
|
|
14
|
+
DEFAULT_MINIMUM_FONT_SIZE = 5
|
|
15
|
+
SHRINK_STEP = 0.5
|
|
16
|
+
|
|
17
|
+
attr_reader :text, :at, :line_height, :ascender, :descender, :leading
|
|
18
|
+
|
|
19
|
+
# Modules every new box of this class extends itself with: a subclass starts with its parent's.
|
|
20
|
+
def self.extensions
|
|
21
|
+
@extensions ||= []
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.inherited(subclass)
|
|
25
|
+
super
|
|
26
|
+
subclass.extensions.concat(extensions)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(formatted_text, options = {})
|
|
30
|
+
@inked = false
|
|
31
|
+
self.class.extensions.reverse_each { extend(_1) }
|
|
32
|
+
Prawn.verify_options(valid_options, options)
|
|
33
|
+
options = options.dup
|
|
34
|
+
@overflow = options[:overflow] || :truncate
|
|
35
|
+
@disable_wrap_by_char = options[:disable_wrap_by_char]
|
|
36
|
+
@original_array = formatted_text
|
|
37
|
+
@text = nil
|
|
38
|
+
@document = options[:document]
|
|
39
|
+
@direction = options[:direction] || @document.text_direction
|
|
40
|
+
@fallback_fonts = options[:fallback_fonts] || @document.fallback_fonts
|
|
41
|
+
@at = (options[:at] || [@document.bounds.left, @document.bounds.top]).dup
|
|
42
|
+
@width = options[:width] || (@document.bounds.right - @at[0])
|
|
43
|
+
@height = options[:height] || default_height
|
|
44
|
+
@align = options[:align] || ((@direction == :rtl) ? :right : :left)
|
|
45
|
+
@vertical_align = options[:valign] || :top
|
|
46
|
+
@leading = options[:leading] || @document.default_leading
|
|
47
|
+
@character_spacing = options[:character_spacing] || @document.character_spacing
|
|
48
|
+
@mode = options[:mode] || @document.text_rendering_mode
|
|
49
|
+
@mode = :fill if @mode == :unknown
|
|
50
|
+
@rotate = options[:rotate] || 0
|
|
51
|
+
@rotate_around = options[:rotate_around] || :upper_left
|
|
52
|
+
@single_line = options[:single_line]
|
|
53
|
+
@draw_text_callback = options[:draw_text_callback]
|
|
54
|
+
if @overflow == :expand
|
|
55
|
+
@height = default_height
|
|
56
|
+
@overflow = :truncate
|
|
57
|
+
end
|
|
58
|
+
@min_font_size = options[:min_font_size] || DEFAULT_MINIMUM_FONT_SIZE
|
|
59
|
+
options[:kerning] = @document.default_kerning? if options[:kerning].nil?
|
|
60
|
+
@options = {kerning: options[:kerning], size: options[:size], style: options[:style]}
|
|
61
|
+
super
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def nothing_printed?
|
|
65
|
+
@nothing_printed
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def everything_printed?
|
|
69
|
+
@everything_printed
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def line_gap
|
|
73
|
+
line_height - (ascender + descender)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def render(flags = {})
|
|
77
|
+
unprinted_text = []
|
|
78
|
+
@laid_out_lines = flags[:keep_layout] ? [] : nil
|
|
79
|
+
with_text_settings do
|
|
80
|
+
text = normalized_text(flags)
|
|
81
|
+
shrink_to_fit(text) if @overflow == :shrink_to_fit
|
|
82
|
+
process_vertical_alignment(text)
|
|
83
|
+
@inked = true unless flags[:dry_run]
|
|
84
|
+
unprinted_text = (@rotate != 0 && @inked) ? render_rotated(text) : wrap(text)
|
|
85
|
+
@inked = false
|
|
86
|
+
end
|
|
87
|
+
unprinted_text.map { |fragment| fragment.merge(text: @document.font.to_utf8(fragment[:text])) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
# Whether the lines of a `render(dry_run: true, keep_layout: true)` can be drawn again as they are.
|
|
91
|
+
def layout_reusable?
|
|
92
|
+
!@laid_out_lines.nil? && @everything_printed && @rotate.zero? && @vertical_align == :top &&
|
|
93
|
+
@overflow != :shrink_to_fit && self.class.extensions.empty? && @original_array.none? { _1[:color] }
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
# Draws, with its top left corner at `at`, the lines the last kept layout wrapped, without wrapping them again.
|
|
97
|
+
def render_laid_out(at)
|
|
98
|
+
@at = at.dup
|
|
99
|
+
with_text_settings do
|
|
100
|
+
@inked = true
|
|
101
|
+
@laid_out_lines.each do |baseline_y, drawn_fragments|
|
|
102
|
+
@baseline_y = baseline_y
|
|
103
|
+
drawn_fragments.each { format_and_draw_fragment(*_1) }
|
|
104
|
+
end
|
|
105
|
+
@inked = false
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def available_width
|
|
110
|
+
@width
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def height
|
|
114
|
+
return 0 if @baseline_y.nil? || @descender.nil?
|
|
115
|
+
|
|
116
|
+
(@baseline_y - @descender).abs
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def draw_fragment(fragment, accumulated_width = 0, line_width = 0, word_spacing = 0)
|
|
120
|
+
x =
|
|
121
|
+
case @align
|
|
122
|
+
when :left then @at[0]
|
|
123
|
+
when :center then @at[0] + (@width * 0.5) - (line_width * 0.5)
|
|
124
|
+
when :right then @at[0] + @width - line_width
|
|
125
|
+
when :justify then (@direction == :ltr) ? @at[0] : @at[0] + @width - line_width
|
|
126
|
+
else raise ArgumentError, "align must be one of :left, :right, :center or :justify symbols"
|
|
127
|
+
end
|
|
128
|
+
x += accumulated_width
|
|
129
|
+
y = @at[1] + @baseline_y + fragment.y_offset
|
|
130
|
+
fragment.left = x
|
|
131
|
+
fragment.baseline = y
|
|
132
|
+
return unless @inked
|
|
133
|
+
|
|
134
|
+
fragment.callback_objects.each { |callback| callback.render_behind(fragment) if callback.respond_to?(:render_behind) }
|
|
135
|
+
@document.word_spacing(word_spacing) do
|
|
136
|
+
if @draw_text_callback
|
|
137
|
+
@draw_text_callback.call(fragment.text, at: [x, y], kerning: @kerning)
|
|
138
|
+
else
|
|
139
|
+
@document.draw_text!(fragment.text, at: [x, y], kerning: @kerning)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
draw_fragment_overlays(fragment)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def valid_options
|
|
146
|
+
VALID_OPTIONS
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
private
|
|
150
|
+
|
|
151
|
+
def with_text_settings(&)
|
|
152
|
+
@document.save_font do
|
|
153
|
+
@document.character_spacing(@character_spacing) do
|
|
154
|
+
@document.text_rendering_mode(@mode) do
|
|
155
|
+
process_options
|
|
156
|
+
@document.font_size(@font_size, &)
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def normalized_text(flags)
|
|
163
|
+
text = normalize_encoding
|
|
164
|
+
text.each { _1.delete(:color) } if flags[:dry_run]
|
|
165
|
+
text
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def original_text
|
|
169
|
+
@original_array.map(&:dup)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def normalize_encoding
|
|
173
|
+
formatted_text = original_text
|
|
174
|
+
formatted_text = formatted_text.flat_map { fragments_by_fallback_font(_1) } unless @fallback_fonts.empty?
|
|
175
|
+
formatted_text.each do |hash|
|
|
176
|
+
if hash[:font]
|
|
177
|
+
@document.font(hash[:font]) { hash[:text] = @document.font.normalize_encoding(hash[:text]) }
|
|
178
|
+
else
|
|
179
|
+
hash[:text] = @document.font.normalize_encoding(hash[:text])
|
|
180
|
+
end
|
|
181
|
+
end
|
|
182
|
+
formatted_text
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def fragments_by_fallback_font(hash)
|
|
186
|
+
original_font = @document.font.family
|
|
187
|
+
fragment_font = hash[:font] || original_font
|
|
188
|
+
fragment_font_style = @arranger.font_style(hash[:styles])
|
|
189
|
+
fragment_font_options = (fragment_font_style == :normal) ? {} : {style: fragment_font_style}
|
|
190
|
+
fallback_fonts = [*@fallback_fonts, fragment_font]
|
|
191
|
+
|
|
192
|
+
font_glyph_pairs = []
|
|
193
|
+
@document.save_font do
|
|
194
|
+
hash[:text].each_char do |character|
|
|
195
|
+
font_glyph_pairs << [font_for_glyph(character, fragment_font, fallback_fonts.dup, fragment_font_options), character]
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
font_glyph_pairs.each { |pair| pair[0] = nil if pair[0] == original_font } if hash[:font].nil?
|
|
199
|
+
|
|
200
|
+
fragments = []
|
|
201
|
+
current_font = nil
|
|
202
|
+
font_glyph_pairs.each do |font, character|
|
|
203
|
+
if font != current_font || fragments.empty?
|
|
204
|
+
current_font = font
|
|
205
|
+
fragment = hash.merge(text: character)
|
|
206
|
+
fragment[:font] = font unless font.nil?
|
|
207
|
+
fragments << fragment
|
|
208
|
+
else
|
|
209
|
+
fragments.last[:text] += character
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
fragments
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def font_for_glyph(character, current_font, fallback_fonts, current_font_options = {})
|
|
216
|
+
loop do
|
|
217
|
+
@document.font(current_font, current_font_options)
|
|
218
|
+
return current_font if fallback_fonts.empty? || @document.font.glyph_present?(character)
|
|
219
|
+
|
|
220
|
+
current_font = fallback_fonts.shift
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def move_baseline_down
|
|
225
|
+
if @baseline_y.zero?
|
|
226
|
+
@baseline_y = -@ascender
|
|
227
|
+
else
|
|
228
|
+
@baseline_y -= @line_height + @leading
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
def default_height
|
|
233
|
+
frame = @document.bounds
|
|
234
|
+
frame = frame.parent while frame.stretchy? && frame.parent
|
|
235
|
+
@at[1] + @document.bounds.absolute_bottom - frame.absolute_bottom
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
def process_vertical_alignment(text)
|
|
239
|
+
return if @vertical_alignment_processed
|
|
240
|
+
|
|
241
|
+
@vertical_alignment_processed = true
|
|
242
|
+
return if @vertical_align == :top
|
|
243
|
+
|
|
244
|
+
wrap(text)
|
|
245
|
+
case @vertical_align
|
|
246
|
+
when :center then @at[1] -= (@height - height + @descender) * 0.5
|
|
247
|
+
when :bottom then @at[1] -= @height - height
|
|
248
|
+
else raise ArgumentError, "valign must be one of :left, :right or :center symbols"
|
|
249
|
+
end
|
|
250
|
+
@height = height
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def shrink_to_fit(text)
|
|
254
|
+
loop do
|
|
255
|
+
if @disable_wrap_by_char && @font_size > @min_font_size
|
|
256
|
+
begin
|
|
257
|
+
wrap(text)
|
|
258
|
+
rescue Errors::CannotFit
|
|
259
|
+
# Too big for a word to fit whole: the next, smaller size may make it fit.
|
|
260
|
+
end
|
|
261
|
+
else
|
|
262
|
+
wrap(text)
|
|
263
|
+
end
|
|
264
|
+
break if @everything_printed || @font_size <= @min_font_size
|
|
265
|
+
|
|
266
|
+
@font_size = [@font_size - SHRINK_STEP, @min_font_size].max
|
|
267
|
+
@document.font_size = @font_size
|
|
268
|
+
end
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
def process_options
|
|
272
|
+
@document.process_text_options(@options)
|
|
273
|
+
@font_size = @options[:size]
|
|
274
|
+
@kerning = @options[:kerning]
|
|
275
|
+
end
|
|
276
|
+
|
|
277
|
+
def render_rotated(text)
|
|
278
|
+
x, y =
|
|
279
|
+
case @rotate_around
|
|
280
|
+
when :center then [@at[0] + (@width * 0.5), @at[1] - (@height * 0.5)]
|
|
281
|
+
when :upper_right then [@at[0] + @width, @at[1]]
|
|
282
|
+
when :lower_right then [@at[0] + @width, @at[1] - @height]
|
|
283
|
+
when :lower_left then [@at[0], @at[1] - @height]
|
|
284
|
+
else [@at[0], @at[1]]
|
|
285
|
+
end
|
|
286
|
+
unprinted_text = ""
|
|
287
|
+
@document.rotate(@rotate, origin: [x, y]) { unprinted_text = wrap(text) }
|
|
288
|
+
unprinted_text
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
def draw_fragment_overlays(fragment)
|
|
292
|
+
@document.stroke_line(fragment.underline_points) if fragment.styles.include?(:underline)
|
|
293
|
+
@document.stroke_line(fragment.strikethrough_points) if fragment.styles.include?(:strikethrough)
|
|
294
|
+
draw_link_annotations(fragment)
|
|
295
|
+
fragment.callback_objects.each { |callback| callback.render_in_front(fragment) if callback.respond_to?(:render_in_front) }
|
|
296
|
+
end
|
|
297
|
+
|
|
298
|
+
def draw_link_annotations(fragment)
|
|
299
|
+
if fragment.link
|
|
300
|
+
@document.link_annotation(fragment.absolute_bounding_box, Border: [0, 0, 0],
|
|
301
|
+
A: {Type: :Action, S: :URI, URI: PDF::Core::LiteralString.new(fragment.link)})
|
|
302
|
+
end
|
|
303
|
+
@document.link_annotation(fragment.absolute_bounding_box, Border: [0, 0, 0], Dest: fragment.anchor) if fragment.anchor
|
|
304
|
+
return unless fragment.local
|
|
305
|
+
|
|
306
|
+
@document.link_annotation(fragment.absolute_bounding_box, Border: [0, 0, 0],
|
|
307
|
+
A: {Type: :Action, S: :Launch, F: PDF::Core::LiteralString.new(fragment.local), NewWindow: true})
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|