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
data/lib/prawnx/text.rb
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module Text
|
|
5
|
+
NBSP = " "
|
|
6
|
+
ZWSP = ""
|
|
7
|
+
SHY = ""
|
|
8
|
+
|
|
9
|
+
VALID_OPTIONS = %i[kerning size style].freeze
|
|
10
|
+
MODES = {fill: 0, stroke: 1, fill_stroke: 2, invisible: 3, fill_clip: 4, stroke_clip: 5, fill_stroke_clip: 6, clip: 7}.freeze
|
|
11
|
+
|
|
12
|
+
class BadFontFamily < StandardError
|
|
13
|
+
def initialize(message = "Bad font family")
|
|
14
|
+
super
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def text(string, options = {})
|
|
19
|
+
return false if string.nil?
|
|
20
|
+
|
|
21
|
+
options = options.dup
|
|
22
|
+
inline_format = options.delete(:inline_format)
|
|
23
|
+
array =
|
|
24
|
+
if inline_format
|
|
25
|
+
text_formatter.format(string, *(inline_format.is_a?(Array) ? inline_format : []))
|
|
26
|
+
else
|
|
27
|
+
[{text: string}]
|
|
28
|
+
end
|
|
29
|
+
formatted_text(array, options)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def formatted_text(array, options = {})
|
|
33
|
+
options = inspect_options_for_text(options.dup)
|
|
34
|
+
color = options.delete(:color)
|
|
35
|
+
array = array.map { it[:color] ? it : it.merge(color:) } if color
|
|
36
|
+
|
|
37
|
+
if @indent_paragraphs
|
|
38
|
+
text_formatter.array_paragraphs(array).each do |paragraph|
|
|
39
|
+
remaining_text = draw_indented_formatted_line(paragraph, options)
|
|
40
|
+
if @no_text_printed && !@all_text_printed
|
|
41
|
+
@bounding_box.move_past_bottom
|
|
42
|
+
remaining_text = draw_indented_formatted_line(paragraph, options)
|
|
43
|
+
end
|
|
44
|
+
next if @all_text_printed
|
|
45
|
+
|
|
46
|
+
remaining_text = fill_formatted_text_box(remaining_text, options)
|
|
47
|
+
draw_remaining_formatted_text_on_new_pages(remaining_text, options)
|
|
48
|
+
end
|
|
49
|
+
else
|
|
50
|
+
remaining_text = fill_formatted_text_box(array, options)
|
|
51
|
+
draw_remaining_formatted_text_on_new_pages(remaining_text, options)
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def text_box(string, options = {})
|
|
56
|
+
options = options.dup
|
|
57
|
+
options[:document] = self
|
|
58
|
+
inline_format = options.delete(:inline_format)
|
|
59
|
+
box =
|
|
60
|
+
if inline_format
|
|
61
|
+
Formatted::Box.new(text_formatter.format(string, *(inline_format.is_a?(Array) ? inline_format : [])), options)
|
|
62
|
+
else
|
|
63
|
+
Box.new(string, options)
|
|
64
|
+
end
|
|
65
|
+
box.render
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def formatted_text_box(array, options = {})
|
|
69
|
+
Formatted::Box.new(array, options.merge(document: self)).render
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def height_of(string, options = {})
|
|
73
|
+
height_of_formatted([{text: string}], options)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def height_of_formatted(array, options = {})
|
|
77
|
+
raise NotImplementedError, ":indent_paragraphs option not available with height_of" if options[:indent_paragraphs]
|
|
78
|
+
|
|
79
|
+
process_final_gap_option(options)
|
|
80
|
+
box = Formatted::Box.new(array, options.merge(height: 100_000_000, document: self))
|
|
81
|
+
box.render(dry_run: true)
|
|
82
|
+
@final_gap ? box.height + box.line_gap + box.leading : box.height
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def draw_text(text, options)
|
|
86
|
+
options = inspect_options_for_draw_text(options.dup)
|
|
87
|
+
text = text.to_s.dup
|
|
88
|
+
save_font do
|
|
89
|
+
process_text_options(options)
|
|
90
|
+
text = font.normalize_encoding(text)
|
|
91
|
+
font_size(options[:size]) { draw_text!(text, options) }
|
|
92
|
+
end
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def draw_text!(text, options)
|
|
96
|
+
unless font.unicode? || font.class.hide_m17n_warning || text.ascii_only?
|
|
97
|
+
warn "PDF's built-in fonts have very limited support for internationalized text.\n" \
|
|
98
|
+
"If you need full UTF-8 support, consider using an external font instead.\n\n" \
|
|
99
|
+
"To disable this warning, add the following line to your code:\nPrawn::Fonts::AFM.hide_m17n_warning = true\n"
|
|
100
|
+
font.class.hide_m17n_warning = true
|
|
101
|
+
end
|
|
102
|
+
x, y = map_to_absolute(options[:at])
|
|
103
|
+
add_text_content(text, x, y, options)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
def process_text_options(options)
|
|
107
|
+
if options[:style]
|
|
108
|
+
raise BadFontFamily unless font.family
|
|
109
|
+
|
|
110
|
+
font(font.family, style: options[:style])
|
|
111
|
+
end
|
|
112
|
+
options[:kerning] = font.has_kerning_data? unless options[:kerning] == false
|
|
113
|
+
options[:size] ||= font_size
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def default_kerning?
|
|
117
|
+
return true unless defined?(@default_kerning)
|
|
118
|
+
|
|
119
|
+
@default_kerning
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def default_kerning(value)
|
|
123
|
+
@default_kerning = value
|
|
124
|
+
end
|
|
125
|
+
alias default_kerning= default_kerning
|
|
126
|
+
|
|
127
|
+
def default_leading(number = nil)
|
|
128
|
+
return @default_leading || 0 if number.nil?
|
|
129
|
+
|
|
130
|
+
@default_leading = number
|
|
131
|
+
end
|
|
132
|
+
alias default_leading= default_leading
|
|
133
|
+
|
|
134
|
+
def text_direction(direction = nil)
|
|
135
|
+
return @text_direction || :ltr if direction.nil?
|
|
136
|
+
|
|
137
|
+
@text_direction = direction
|
|
138
|
+
end
|
|
139
|
+
alias text_direction= text_direction
|
|
140
|
+
|
|
141
|
+
def fallback_fonts(fallback_fonts = nil)
|
|
142
|
+
return @fallback_fonts || [] if fallback_fonts.nil?
|
|
143
|
+
|
|
144
|
+
@fallback_fonts = fallback_fonts
|
|
145
|
+
end
|
|
146
|
+
alias fallback_fonts= fallback_fonts
|
|
147
|
+
|
|
148
|
+
def text_rendering_mode(mode = nil, &)
|
|
149
|
+
return @text_rendering_mode || :fill if mode.nil?
|
|
150
|
+
raise ArgumentError, "mode must be between one of #{MODES.keys.join(", ")} (#{mode})" unless MODES.key?(mode)
|
|
151
|
+
return yield if text_rendering_mode == mode
|
|
152
|
+
|
|
153
|
+
wrap_and_restore(:@text_rendering_mode, mode, -> { "\n#{MODES[text_rendering_mode]} Tr" }, &)
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
def forget_text_rendering_mode!
|
|
157
|
+
@text_rendering_mode = :unknown
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def character_spacing(amount = nil, &)
|
|
161
|
+
return @character_spacing || 0 if amount.nil?
|
|
162
|
+
return yield if character_spacing == amount
|
|
163
|
+
|
|
164
|
+
wrap_and_restore(:@character_spacing, amount, -> { "\n#{PDF.real(character_spacing)} Tc" }, &)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def word_spacing(amount = nil, &)
|
|
168
|
+
return @word_spacing || 0 if amount.nil?
|
|
169
|
+
return yield if word_spacing == amount
|
|
170
|
+
|
|
171
|
+
wrap_and_restore(:@word_spacing, amount, -> { "\n#{PDF.real(word_spacing)} Tw" }, &)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def horizontal_text_scaling(amount = nil, &)
|
|
175
|
+
return @horizontal_text_scaling || 100 if amount.nil?
|
|
176
|
+
return yield if horizontal_text_scaling == amount
|
|
177
|
+
|
|
178
|
+
wrap_and_restore(:@horizontal_text_scaling, amount, -> { "\n#{PDF.real(horizontal_text_scaling)} Tz" }, &)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def rise(amount = nil, &)
|
|
182
|
+
return @rise || 0 if amount.nil?
|
|
183
|
+
return yield if rise == amount
|
|
184
|
+
|
|
185
|
+
wrap_and_restore(:@rise, amount, -> { "\n#{PDF.real(rise)} Ts" }, &)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
def add_text_content(text, x, y, options)
|
|
189
|
+
chunks = font.encode_text(text, options.merge(word_spacing:, font_size:))
|
|
190
|
+
add_content("\nBT")
|
|
191
|
+
if options[:rotate]
|
|
192
|
+
radians = Float(options[:rotate]) * Math::PI / 180
|
|
193
|
+
add_content("#{PDF.real_params([Math.cos(radians), Math.sin(radians), -Math.sin(radians), Math.cos(radians), x, y])} Tm")
|
|
194
|
+
else
|
|
195
|
+
add_content("#{PDF.real(x)} #{PDF.real(y)} Td")
|
|
196
|
+
end
|
|
197
|
+
chunks.each do |subset, string|
|
|
198
|
+
font.add_to_current_page(subset)
|
|
199
|
+
add_content("#{PDF.serialize(font.identifier_for(subset), true)} #{PDF.serialize(font_size, true)} Tf")
|
|
200
|
+
operator = string.is_a?(Array) ? "TJ" : "Tj"
|
|
201
|
+
add_content("#{PDF.serialize(string, true)} #{operator}")
|
|
202
|
+
end
|
|
203
|
+
add_content("ET\n")
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
private
|
|
207
|
+
|
|
208
|
+
def draw_remaining_formatted_text_on_new_pages(remaining_text, options)
|
|
209
|
+
until remaining_text.empty?
|
|
210
|
+
@bounding_box.move_past_bottom
|
|
211
|
+
previous_remaining_text = remaining_text
|
|
212
|
+
remaining_text = fill_formatted_text_box(remaining_text, options)
|
|
213
|
+
break if remaining_text == previous_remaining_text
|
|
214
|
+
end
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def draw_indented_formatted_line(string, options)
|
|
218
|
+
gap = options.fetch(:direction, text_direction) == :ltr ? [@indent_paragraphs, 0] : [0, @indent_paragraphs]
|
|
219
|
+
indent(*gap) { fill_formatted_text_box(string, options.merge(single_line: true)) }
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def fill_formatted_text_box(text, options)
|
|
223
|
+
merge_text_box_positioning_options(options)
|
|
224
|
+
box = Formatted::Box.new(text, options)
|
|
225
|
+
remaining_text = box.render
|
|
226
|
+
@no_text_printed = box.nothing_printed?
|
|
227
|
+
@all_text_printed = box.everything_printed?
|
|
228
|
+
self.y -= box.height
|
|
229
|
+
self.y -= box.line_gap + box.leading if @final_gap || (options[:single_line] && !@all_text_printed)
|
|
230
|
+
remaining_text
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def merge_text_box_positioning_options(options)
|
|
234
|
+
bottom = @bounding_box.stretchy? ? @margin_box.absolute_bottom : @bounding_box.absolute_bottom
|
|
235
|
+
options[:height] = y - bottom
|
|
236
|
+
options[:width] = bounds.width
|
|
237
|
+
options[:at] = [@bounding_box.left_side - @bounding_box.absolute_left, y - @bounding_box.absolute_bottom]
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
def inspect_options_for_text(options)
|
|
241
|
+
raise ArgumentError, ":at is no longer a valid option with text.use draw_text or text_box instead" if options[:at]
|
|
242
|
+
|
|
243
|
+
process_final_gap_option(options)
|
|
244
|
+
@indent_paragraphs = options.delete(:indent_paragraphs)
|
|
245
|
+
options[:document] = self
|
|
246
|
+
options
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def process_final_gap_option(options)
|
|
250
|
+
@final_gap = options[:final_gap].nil? || options[:final_gap]
|
|
251
|
+
options.delete(:final_gap)
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def move_text_position(amount)
|
|
255
|
+
bottom = @bounding_box.stretchy? ? @margin_box.absolute_bottom : @bounding_box.absolute_bottom
|
|
256
|
+
@bounding_box.move_past_bottom if (y - amount) < bottom
|
|
257
|
+
self.y -= amount
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
def wrap_and_restore(variable, value, operator)
|
|
261
|
+
original_value = instance_variable_get(variable)
|
|
262
|
+
instance_variable_set(variable, value)
|
|
263
|
+
add_content(operator.call)
|
|
264
|
+
begin
|
|
265
|
+
yield
|
|
266
|
+
ensure
|
|
267
|
+
instance_variable_set(variable, original_value)
|
|
268
|
+
add_content(operator.call)
|
|
269
|
+
end
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def inspect_options_for_draw_text(options)
|
|
273
|
+
raise ArgumentError, "The :at option is required for draw_text" if options[:at].nil?
|
|
274
|
+
raise ArgumentError, "The :align option does not work with draw_text" if options[:align]
|
|
275
|
+
|
|
276
|
+
options[:kerning] = default_kerning? if options[:kerning].nil?
|
|
277
|
+
PrawnX.verify_options(VALID_OPTIONS + %i[at rotate], options)
|
|
278
|
+
options
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
# The current transformation matrix, tracked alongside the graphic state so gradients and soft masks can be
|
|
5
|
+
# placed in page space.
|
|
6
|
+
module TransformationStack
|
|
7
|
+
IDENTITY = [1, 0, 0, 1, 0, 0].freeze
|
|
8
|
+
|
|
9
|
+
def add_to_transformation_stack(a, b, c, d, e, f)
|
|
10
|
+
@transformation_stack ||= [IDENTITY.dup]
|
|
11
|
+
@transformation_stack.push(multiply(@transformation_stack.last, [a, b, c, d, e, f]))
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def save_transformation_stack
|
|
15
|
+
@transformation_stack ||= [IDENTITY.dup]
|
|
16
|
+
@saved_transformation_stack_lengths ||= []
|
|
17
|
+
@saved_transformation_stack_lengths.push(@transformation_stack.length)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def restore_transformation_stack
|
|
21
|
+
return if @saved_transformation_stack_lengths.nil? || @saved_transformation_stack_lengths.empty?
|
|
22
|
+
|
|
23
|
+
@transformation_stack.pop(@transformation_stack.length - @saved_transformation_stack_lengths.pop)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def current_transformation_matrix_with_translation(x = 0, y = 0)
|
|
27
|
+
a, b, c, d, e, f = (@transformation_stack || [IDENTITY]).last
|
|
28
|
+
[a, b, c, d, (a * x) + (c * y) + e, (b * x) + (d * y) + f].map { Float(it) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
def multiply(first, second)
|
|
34
|
+
a1, b1, c1, d1, e1, f1 = first
|
|
35
|
+
a2, b2, c2, d2, e2, f2 = second
|
|
36
|
+
[
|
|
37
|
+
(a2 * a1) + (b2 * c1), (a2 * b1) + (b2 * d1),
|
|
38
|
+
(c2 * a1) + (d2 * c1), (c2 * b1) + (d2 * d1),
|
|
39
|
+
(e2 * a1) + (f2 * c1) + e1, (e2 * b1) + (f2 * d1) + f1,
|
|
40
|
+
]
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
module TrueType
|
|
5
|
+
# A Compact Font Format program (Adobe Technical Note #5176) keeping only the outlines a document uses: other
|
|
6
|
+
# glyphs become an empty charstring, so glyph indices and subroutines stay where they were.
|
|
7
|
+
class CffSubset
|
|
8
|
+
ENDCHAR = "\x0E".b
|
|
9
|
+
CHARSTRINGS = [17].freeze
|
|
10
|
+
PRIVATE = [18].freeze
|
|
11
|
+
CHARSET = [15].freeze
|
|
12
|
+
ENCODING = [16].freeze
|
|
13
|
+
FD_ARRAY = [12, 36].freeze
|
|
14
|
+
FD_SELECT = [12, 37].freeze
|
|
15
|
+
# Offsets 0 to 2 of charset and Encoding name predefined tables, not places in the font.
|
|
16
|
+
PREDEFINED_TABLE_LIMIT = 2
|
|
17
|
+
FIVE_BYTE_INTEGER = 29
|
|
18
|
+
|
|
19
|
+
def initialize(cff)
|
|
20
|
+
@cff = cff.b
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def bytes(glyphs_kept)
|
|
24
|
+
build(glyphs_kept)
|
|
25
|
+
rescue *File::MALFORMED_FONT_ERRORS => exception
|
|
26
|
+
raise Error, "Malformed CFF font program: #{exception.message}"
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def build(glyphs_kept)
|
|
32
|
+
header_size = @cff.getbyte(2)
|
|
33
|
+
_names, names_end = read_index(header_size)
|
|
34
|
+
top_dicts, top_end = read_index(names_end)
|
|
35
|
+
top_operators = parse_dict(top_dicts.first)
|
|
36
|
+
|
|
37
|
+
old_charstrings_offset = operands(top_operators, CHARSTRINGS).first
|
|
38
|
+
charstrings, = read_index(old_charstrings_offset)
|
|
39
|
+
kept = glyphs_kept.to_h { [it, true] }
|
|
40
|
+
new_charstrings = charstrings.each_with_index.map { |charstring, glyph| kept[glyph] ? charstring : ENDCHAR }
|
|
41
|
+
|
|
42
|
+
font_dicts = nil
|
|
43
|
+
if (fd_array_offset = operands(top_operators, FD_ARRAY).first)
|
|
44
|
+
font_dicts = read_index(fd_array_offset).first.map { parse_dict(it) }
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Every offset operand is written as a five-byte integer, so the new Top DICT's size is known before its
|
|
48
|
+
# offsets are: everything after it then moves by the same delta.
|
|
49
|
+
placeholder_top = encode_dict(relocated(top_operators, 0, 0, 0))
|
|
50
|
+
delta = index([placeholder_top]).bytesize - (top_end - names_end)
|
|
51
|
+
tail = @cff.byteslice(top_end..)
|
|
52
|
+
charstrings_offset = top_end + delta + tail.bytesize
|
|
53
|
+
appended = index(new_charstrings)
|
|
54
|
+
fd_array_new_offset = nil
|
|
55
|
+
if font_dicts
|
|
56
|
+
fd_array_new_offset = charstrings_offset + appended.bytesize
|
|
57
|
+
appended += index(font_dicts.map { encode_dict(relocated(it, delta, nil, nil)) })
|
|
58
|
+
end
|
|
59
|
+
new_top = encode_dict(relocated(top_operators, delta, charstrings_offset, fd_array_new_offset))
|
|
60
|
+
@cff.byteslice(0, names_end) + index([new_top]) + tail + appended
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def read_index(offset)
|
|
64
|
+
count = @cff.unpack1("@#{offset} n")
|
|
65
|
+
return [[], offset + 2] if count.zero?
|
|
66
|
+
|
|
67
|
+
offset_size = @cff.getbyte(offset + 2)
|
|
68
|
+
offsets = Array.new(count + 1) { read_offset(offset + 3 + (it * offset_size), offset_size) }
|
|
69
|
+
data_start = offset + 3 + ((count + 1) * offset_size) - 1
|
|
70
|
+
items = offsets.each_cons(2).map { |first, last| @cff.byteslice(data_start + first, last - first) }
|
|
71
|
+
[items, data_start + offsets.last]
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def read_offset(position, size)
|
|
75
|
+
@cff.byteslice(position, size).bytes.inject(0) { (_1 << 8) | _2 }
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def index(items)
|
|
79
|
+
return [0].pack("n") if items.empty?
|
|
80
|
+
|
|
81
|
+
offsets = items.each_with_object([1]) { |item, list| list << (list.last + item.bytesize) }
|
|
82
|
+
offset_size = [1, 2, 3, 4].find { offsets.last < (1 << (8 * it)) }
|
|
83
|
+
packed_offsets = offsets.map { [it].pack("N").byteslice(4 - offset_size, offset_size) }.join
|
|
84
|
+
[items.size].pack("n") + offset_size.chr + packed_offsets + items.join.b
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# [operator bytes, operand values, raw operand bytes] in order.
|
|
88
|
+
def parse_dict(data)
|
|
89
|
+
entries = []
|
|
90
|
+
operands = []
|
|
91
|
+
raw_start = 0
|
|
92
|
+
position = 0
|
|
93
|
+
while position < data.bytesize
|
|
94
|
+
byte = data.getbyte(position)
|
|
95
|
+
if byte <= 21
|
|
96
|
+
operator = byte == 12 ? [12, data.getbyte(position + 1)] : [byte]
|
|
97
|
+
position += operator.size
|
|
98
|
+
entries << [operator, operands, data.byteslice(raw_start, position - operator.size - raw_start)]
|
|
99
|
+
operands = []
|
|
100
|
+
raw_start = position
|
|
101
|
+
else
|
|
102
|
+
value, size = read_operand(data, position)
|
|
103
|
+
operands << value
|
|
104
|
+
position += size
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
entries
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def read_operand(data, position)
|
|
111
|
+
byte = data.getbyte(position)
|
|
112
|
+
case byte
|
|
113
|
+
when 28 then [data.unpack1("@#{position + 1} s>"), 3]
|
|
114
|
+
when 29 then [data.unpack1("@#{position + 1} l>"), 5]
|
|
115
|
+
when 30
|
|
116
|
+
size = 1
|
|
117
|
+
loop do
|
|
118
|
+
nibbles = data.getbyte(position + size)
|
|
119
|
+
size += 1
|
|
120
|
+
break if (nibbles >> 4) == 0x0F || (nibbles & 0x0F) == 0x0F
|
|
121
|
+
end
|
|
122
|
+
[:real, size]
|
|
123
|
+
when 32..246 then [byte - 139, 1]
|
|
124
|
+
when 247..250 then [((byte - 247) * 256) + data.getbyte(position + 1) + 108, 2]
|
|
125
|
+
when 251..254 then [(-(byte - 251) * 256) - data.getbyte(position + 1) - 108, 2]
|
|
126
|
+
else raise Error, "Unexpected byte #{byte} in a CFF dictionary"
|
|
127
|
+
end
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def operands(entries, operator)
|
|
131
|
+
entries.find { it[0] == operator }&.dig(1) || []
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
# Offsets into the font move by delta; CharStrings and FDArray point at their new copies at the end.
|
|
135
|
+
def relocated(entries, delta, charstrings_offset, fd_array_offset)
|
|
136
|
+
entries.map do |operator, values, raw|
|
|
137
|
+
new_values =
|
|
138
|
+
case operator
|
|
139
|
+
when CHARSTRINGS then charstrings_offset && [charstrings_offset]
|
|
140
|
+
when FD_ARRAY then fd_array_offset && [fd_array_offset]
|
|
141
|
+
when PRIVATE then [values[0], values[1] + delta]
|
|
142
|
+
when CHARSET, ENCODING then [values[0] > PREDEFINED_TABLE_LIMIT ? values[0] + delta : values[0]]
|
|
143
|
+
when FD_SELECT then [values[0] + delta]
|
|
144
|
+
end
|
|
145
|
+
[operator, new_values, raw]
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def encode_dict(entries)
|
|
150
|
+
entries.each_with_object(+"".b) do |(operator, new_values, raw), dict|
|
|
151
|
+
dict << (new_values ? new_values.map { [FIVE_BYTE_INTEGER, it].pack("Cl>") }.join : raw)
|
|
152
|
+
dict << operator.pack("C*")
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
end
|