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,312 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
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(it) }
|
|
32
|
+
PrawnX.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(formatted_text, options)
|
|
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 { it.merge(text: @document.font.to_utf8(it[: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? { it[: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(*it) }
|
|
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 { it.render_behind(fragment) if it.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 { it.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(it) } 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 { it[0] = nil if it[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 { it.render_in_front(fragment) if it.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::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::LiteralString.new(fragment.local), NewWindow: true})
|
|
308
|
+
end
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
end
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
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
|