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,268 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
class Document
|
|
5
|
+
include Internals
|
|
6
|
+
include Graphics
|
|
7
|
+
include Text
|
|
8
|
+
include TransformationStack
|
|
9
|
+
include PDF::Core::Annotations
|
|
10
|
+
include PDF::Core::Destinations
|
|
11
|
+
include Security
|
|
12
|
+
include Images
|
|
13
|
+
include Gamba::PdfA
|
|
14
|
+
include Table::Interface
|
|
15
|
+
include Stamp
|
|
16
|
+
include SoftMask
|
|
17
|
+
|
|
18
|
+
VALID_OPTIONS = %i[
|
|
19
|
+
page_size page_layout margin left_margin right_margin top_margin bottom_margin skip_page_creation compress
|
|
20
|
+
background info text_formatter print_scaling pdf_a
|
|
21
|
+
].freeze
|
|
22
|
+
|
|
23
|
+
attr_accessor :margin_box, :page_number, :text_formatter
|
|
24
|
+
attr_reader :margins, :y
|
|
25
|
+
|
|
26
|
+
# Modules every new document extends itself with: a subclass starts with its parent's.
|
|
27
|
+
def self.extensions
|
|
28
|
+
@extensions ||= []
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def self.inherited(subclass)
|
|
32
|
+
super
|
|
33
|
+
subclass.extensions.concat(extensions)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def self.generate(filename, options = {}, &)
|
|
37
|
+
new(options, &).render_file(filename)
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def initialize(options = {}, &block)
|
|
41
|
+
options = options.dup
|
|
42
|
+
Prawn.verify_options(VALID_OPTIONS, options)
|
|
43
|
+
self.class.extensions.reverse_each { extend(_1) }
|
|
44
|
+
|
|
45
|
+
initialize_internals(options)
|
|
46
|
+
@pdf_a = options.delete(:pdf_a) == true
|
|
47
|
+
@background = options[:background]
|
|
48
|
+
@background_scale = options[:background_scale] || 1
|
|
49
|
+
@font_size = 12
|
|
50
|
+
@bounding_box = nil
|
|
51
|
+
@margin_box = nil
|
|
52
|
+
@page_number = 0
|
|
53
|
+
@text_formatter = options.delete(:text_formatter) || Text::Formatted::Parser
|
|
54
|
+
options[:size] = options.delete(:page_size)
|
|
55
|
+
options[:layout] = options.delete(:page_layout)
|
|
56
|
+
|
|
57
|
+
initialize_first_page(options)
|
|
58
|
+
@bounding_box = @margin_box
|
|
59
|
+
|
|
60
|
+
return unless block
|
|
61
|
+
|
|
62
|
+
(block.arity < 1) ? instance_eval(&block) : yield(self)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def start_new_page(options = {})
|
|
66
|
+
last_page = page
|
|
67
|
+
page_options = {size: options[:size] || last_page&.size, layout: options[:layout] || last_page&.layout, margins: last_page&.margins&.dup}
|
|
68
|
+
if last_page
|
|
69
|
+
new_graphic_state = last_page.graphic_state&.dup
|
|
70
|
+
new_graphic_state&.color_space = {}
|
|
71
|
+
last_blend_mode = new_graphic_state&.blend_mode
|
|
72
|
+
new_graphic_state&.blend_mode = nil
|
|
73
|
+
new_graphic_state&.reset_text_state
|
|
74
|
+
page_options[:graphic_state] = new_graphic_state
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
@page = PDF::Core::Page.new(self, page_options)
|
|
78
|
+
apply_margin_options(options)
|
|
79
|
+
generate_margin_box
|
|
80
|
+
@bounding_box = @margin_box if last_page && (last_page.size != page.size || last_page.layout != page.layout)
|
|
81
|
+
use_graphic_settings
|
|
82
|
+
resume_text_state
|
|
83
|
+
return if options[:orphan]
|
|
84
|
+
|
|
85
|
+
insert_page(page, @page_number)
|
|
86
|
+
@page_number += 1
|
|
87
|
+
canvas { image(@background, scale: @background_scale, at: bounds.top_left) } if @background
|
|
88
|
+
@y = @bounding_box.absolute_top
|
|
89
|
+
float { @on_page_create&.call(self) }
|
|
90
|
+
header_blend_mode = page.stack.last&.blend_mode
|
|
91
|
+
reopen_ext_gstate_blocks
|
|
92
|
+
# The blend mode in effect on the last page, which the new one resets to Normal (ISO 32000-1 §8.4.1), once the
|
|
93
|
+
# blocks still open are reopened, unless the page's header set one of its own.
|
|
94
|
+
return if header_blend_mode || graphic_state.blend_mode == last_blend_mode
|
|
95
|
+
|
|
96
|
+
write_ext_gstate(last_blend_mode || blend_mode_ext_gstate(:Normal))
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def delete_page(index)
|
|
100
|
+
return false if index.abs > pages.count - 1
|
|
101
|
+
|
|
102
|
+
pages.delete_at(index)
|
|
103
|
+
pages_reference.data[:Kids].delete_at(index)
|
|
104
|
+
pages_reference.data[:Count] -= 1
|
|
105
|
+
@page_number -= 1
|
|
106
|
+
true
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def page_count
|
|
110
|
+
pages.size
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def go_to_page(page_number)
|
|
114
|
+
@page_number = page_number
|
|
115
|
+
@page = pages[page_number - 1]
|
|
116
|
+
generate_margin_box
|
|
117
|
+
@y = @bounding_box.absolute_top
|
|
118
|
+
close_restored_graphics_states
|
|
119
|
+
reopen_ext_gstate_blocks_on_existing_page
|
|
120
|
+
resume_text_state
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def y=(new_y)
|
|
124
|
+
@y = new_y
|
|
125
|
+
bounds.update_height
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def cursor
|
|
129
|
+
y - bounds.absolute_bottom
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
def move_cursor_to(new_y)
|
|
133
|
+
self.y = new_y + bounds.absolute_bottom
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def float
|
|
137
|
+
original_page = page_number
|
|
138
|
+
original_y = y
|
|
139
|
+
yield
|
|
140
|
+
go_to_page(original_page) unless page_number == original_page
|
|
141
|
+
self.y = original_y
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def render(*arguments)
|
|
145
|
+
output = arguments.first
|
|
146
|
+
(1..page_count).each do |page_number|
|
|
147
|
+
go_to_page(page_number)
|
|
148
|
+
repeaters.each { _1.run(page_number) }
|
|
149
|
+
end
|
|
150
|
+
pdf = render_document
|
|
151
|
+
output << pdf if output.respond_to?(:<<)
|
|
152
|
+
pdf
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def svg(data, options = {}, &)
|
|
156
|
+
require "prawn-svg"
|
|
157
|
+
Prawn::SVG::Extension.instance_method(:svg).bind_call(self, data, options, &)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def render_file(filename)
|
|
161
|
+
File.binwrite(filename, render)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def bounds
|
|
165
|
+
@bounding_box
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def bounds=(bounding_box)
|
|
169
|
+
@bounding_box = bounding_box
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
def reference_bounds
|
|
173
|
+
@bounding_box.reference_bounds
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def move_up(amount)
|
|
177
|
+
self.y += amount
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def move_down(amount)
|
|
181
|
+
self.y -= amount
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
def pad_top(amount)
|
|
185
|
+
move_down(amount)
|
|
186
|
+
yield
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def pad_bottom(amount)
|
|
190
|
+
yield
|
|
191
|
+
move_down(amount)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def pad(amount)
|
|
195
|
+
move_down(amount)
|
|
196
|
+
yield
|
|
197
|
+
move_down(amount)
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def indent(left, right = 0, &)
|
|
201
|
+
bounds.indent(left, right, &)
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def page_match?(page_filter, page_number)
|
|
205
|
+
case page_filter
|
|
206
|
+
when :all then true
|
|
207
|
+
when :odd then page_number.odd?
|
|
208
|
+
when :even then page_number.even?
|
|
209
|
+
when Range, Array then page_filter.include?(page_number)
|
|
210
|
+
when Proc then page_filter.call(page_number)
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def group(*)
|
|
215
|
+
raise NotImplementedError, "Document#group has been disabled because its implementation lead to corrupted documents whenever a page boundary was crossed"
|
|
216
|
+
end
|
|
217
|
+
|
|
218
|
+
def transaction
|
|
219
|
+
raise NotImplementedError, "Document#transaction has been disabled because its implementation lead to corrupted documents whenever a page boundary was crossed"
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def initialize_first_page(options)
|
|
223
|
+
start_new_page(options[:skip_page_creation] ? options.merge(orphan: true) : options)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
def repeaters
|
|
227
|
+
@repeaters ||= []
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
private
|
|
231
|
+
|
|
232
|
+
# A gradient set as the fill or stroke stays it on the new page, in place of the colour Prawn writes.
|
|
233
|
+
def use_graphic_settings(override_settings = false)
|
|
234
|
+
write_fill_paint if graphic_state.fill_pattern || current_fill_color != "000000" || override_settings
|
|
235
|
+
write_stroke_paint if graphic_state.stroke_pattern || current_stroke_color != "000000" || override_settings
|
|
236
|
+
write_line_width if line_width != 1 || override_settings
|
|
237
|
+
write_stroke_cap_style if cap_style != :butt || override_settings
|
|
238
|
+
write_stroke_join_style if join_style != :miter || override_settings
|
|
239
|
+
write_stroke_dash if dashed? || override_settings
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
def generate_margin_box
|
|
243
|
+
old_margin_box = @margin_box
|
|
244
|
+
_left, _bottom, page_width, page_height = page.dimensions
|
|
245
|
+
@margin_box = BoundingBox.new(
|
|
246
|
+
self, nil, [page.margins[:left], page_height - page.margins[:top]],
|
|
247
|
+
width: page_width - (page.margins[:left] + page.margins[:right]),
|
|
248
|
+
height: page_height - (page.margins[:top] + page.margins[:bottom])
|
|
249
|
+
)
|
|
250
|
+
if old_margin_box
|
|
251
|
+
@margin_box.add_left_padding(old_margin_box.total_left_padding)
|
|
252
|
+
@margin_box.add_right_padding(old_margin_box.total_right_padding)
|
|
253
|
+
end
|
|
254
|
+
@bounding_box = @margin_box unless @bounding_box&.parent
|
|
255
|
+
end
|
|
256
|
+
|
|
257
|
+
MARGIN_POSITIONS_BY_COUNT = {4 => [0, 1, 2, 3], 3 => [0, 1, 2, 1], 2 => [0, 1, 0, 1], 1 => [0, 0, 0, 0], 0 => []}.freeze
|
|
258
|
+
|
|
259
|
+
def apply_margin_options(options)
|
|
260
|
+
margin = Array(options[:margin])
|
|
261
|
+
positions = MARGIN_POSITIONS_BY_COUNT[margin.length]
|
|
262
|
+
%i[top right bottom left].zip(positions).each do |side, position|
|
|
263
|
+
new_margin = options[:"#{side}_margin"] || (margin[position] if position)
|
|
264
|
+
page.margins[side] = new_margin if new_margin
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
end
|
|
268
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
module Encoding
|
|
5
|
+
# WinAnsiEncoding, ISO 32000-1 Annex D.2: the glyph name of each Windows-1252 byte, .notdef where it has none.
|
|
6
|
+
module WinAnsi
|
|
7
|
+
CHARACTERS = [
|
|
8
|
+
*Array.new(32, ".notdef"),
|
|
9
|
+
"space", "exclam", "quotedbl", "numbersign", "dollar", "percent", "ampersand", "quotesingle", "parenleft", "parenright", "asterisk", "plus", "comma", "hyphen", "period", "slash", "zero", "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "colon", "semicolon", "less", "equal", "greater", "question", "at", "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "bracketleft", "backslash", "bracketright", "asciicircum", "underscore", "grave", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "braceleft", "bar", "braceright", "asciitilde", ".notdef", "Euro", ".notdef", "quotesinglbase", "florin", "quotedblbase", "ellipsis", "dagger", "daggerdbl", "circumflex", "perthousand", "Scaron", "guilsinglleft", "OE", ".notdef", "Zcaron", ".notdef", ".notdef", "quoteleft", "quoteright", "quotedblleft", "quotedblright", "bullet", "endash", "emdash", "tilde", "trademark", "scaron", "guilsinglright", "oe", ".notdef", "zcaron", "Ydieresis", "space", "exclamdown", "cent", "sterling", "currency", "yen", "brokenbar", "section", "dieresis", "copyright", "ordfeminine", "guillemotleft", "logicalnot", "hyphen", "registered", "macron", "degree", "plusminus", "twosuperior", "threesuperior", "acute", "mu", "paragraph", "periodcentered", "cedilla", "onesuperior", "ordmasculine", "guillemotright", "onequarter", "onehalf", "threequarters", "questiondown", "Agrave", "Aacute", "Acircumflex", "Atilde", "Adieresis", "Aring", "AE", "Ccedilla", "Egrave", "Eacute", "Ecircumflex", "Edieresis", "Igrave", "Iacute", "Icircumflex", "Idieresis", "Eth", "Ntilde", "Ograve", "Oacute", "Ocircumflex", "Otilde", "Odieresis", "multiply", "Oslash", "Ugrave", "Uacute", "Ucircumflex", "Udieresis", "Yacute", "Thorn", "germandbls", "agrave", "aacute", "acircumflex", "atilde", "adieresis", "aring", "ae", "ccedilla", "egrave", "eacute", "ecircumflex", "edieresis", "igrave", "iacute", "icircumflex", "idieresis", "eth", "ntilde", "ograve", "oacute", "ocircumflex", "otilde", "odieresis", "divide", "oslash", "ugrave", "uacute", "ucircumflex", "udieresis", "yacute", "thorn", "ydieresis"
|
|
10
|
+
].freeze
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
data/lib/gamba/errors.rb
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Errors Prawn raises from PDF::Core keep that name: apps rescue them by it.
|
|
4
|
+
module PDF
|
|
5
|
+
module Core
|
|
6
|
+
module Errors
|
|
7
|
+
EmptyGraphicStateStack = Class.new(StandardError)
|
|
8
|
+
FailedObjectConversion = Class.new(StandardError)
|
|
9
|
+
InvalidPageLayout = Class.new(StandardError)
|
|
10
|
+
end
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
module Prawn
|
|
15
|
+
module Errors
|
|
16
|
+
EmptyGraphicStateStack = PDF::Core::Errors::EmptyGraphicStateStack
|
|
17
|
+
FailedObjectConversion = PDF::Core::Errors::FailedObjectConversion
|
|
18
|
+
InvalidPageLayout = PDF::Core::Errors::InvalidPageLayout
|
|
19
|
+
BlockRequired = Class.new(StandardError)
|
|
20
|
+
CannotFit = Class.new(StandardError)
|
|
21
|
+
CannotGroup = Class.new(StandardError)
|
|
22
|
+
EncryptedPDF = Class.new(StandardError)
|
|
23
|
+
IncompatibleStringEncoding = Class.new(StandardError)
|
|
24
|
+
InvalidGraphicsPath = Class.new(StandardError)
|
|
25
|
+
InvalidJoinStyle = Class.new(StandardError)
|
|
26
|
+
InvalidName = Class.new(StandardError)
|
|
27
|
+
InvalidTableSpan = Class.new(StandardError)
|
|
28
|
+
MalformedPDF = Class.new(StandardError)
|
|
29
|
+
NameTaken = Class.new(StandardError)
|
|
30
|
+
NotOnPage = Class.new(StandardError)
|
|
31
|
+
RequiredOption = Class.new(StandardError)
|
|
32
|
+
UndefinedObjectName = Class.new(StandardError)
|
|
33
|
+
UnknownFont = Class.new(StandardError)
|
|
34
|
+
UnknownOption = Class.new(StandardError)
|
|
35
|
+
UnknownOutlineTitle = Class.new(StandardError)
|
|
36
|
+
UnrecognizedTableContent = Class.new(StandardError)
|
|
37
|
+
UnsupportedImageType = Class.new(StandardError)
|
|
38
|
+
end
|
|
39
|
+
end
|
data/lib/gamba/font.rb
ADDED
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prawn
|
|
4
|
+
class Document
|
|
5
|
+
DEFAULT_FONT_OPTIONS = {}.freeze
|
|
6
|
+
BUILT_IN_FONT_FAMILIES = {
|
|
7
|
+
"Courier" => {bold: "Courier-Bold", italic: "Courier-Oblique", bold_italic: "Courier-BoldOblique", normal: "Courier"},
|
|
8
|
+
"Times-Roman" => {bold: "Times-Bold", italic: "Times-Italic", bold_italic: "Times-BoldItalic", normal: "Times-Roman"},
|
|
9
|
+
"Helvetica" => {bold: "Helvetica-Bold", italic: "Helvetica-Oblique", bold_italic: "Helvetica-BoldOblique", normal: "Helvetica"}
|
|
10
|
+
}.freeze
|
|
11
|
+
|
|
12
|
+
def font(name = nil, options = DEFAULT_FONT_OPTIONS)
|
|
13
|
+
return @font || font("Helvetica") if name.nil?
|
|
14
|
+
raise Errors::NotOnPage if pages.empty? && !page.in_stamp_stream?
|
|
15
|
+
|
|
16
|
+
new_font = find_font(name.to_s, options)
|
|
17
|
+
if block_given?
|
|
18
|
+
save_font do
|
|
19
|
+
set_font(new_font, options[:size])
|
|
20
|
+
yield
|
|
21
|
+
end
|
|
22
|
+
else
|
|
23
|
+
set_font(new_font, options[:size])
|
|
24
|
+
end
|
|
25
|
+
@font
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def font_size(points = nil)
|
|
29
|
+
return @font_size unless points
|
|
30
|
+
|
|
31
|
+
size_before_yield = @font_size
|
|
32
|
+
@font_size = points
|
|
33
|
+
return unless block_given?
|
|
34
|
+
|
|
35
|
+
yield
|
|
36
|
+
@font_size = size_before_yield
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def font_size=(size)
|
|
40
|
+
font_size(size)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def width_of(string, options = {})
|
|
44
|
+
return width_of_string(string, options) unless options.key?(:inline_format)
|
|
45
|
+
|
|
46
|
+
inline_format = options[:inline_format]
|
|
47
|
+
arranger = Text::Formatted::Arranger.new(self, options)
|
|
48
|
+
arranger.consumed = text_formatter.format(string, *(inline_format.is_a?(Array) ? inline_format : []))
|
|
49
|
+
arranger.finalize_line
|
|
50
|
+
arranger.line_width
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def font_families
|
|
54
|
+
@font_families ||= BUILT_IN_FONT_FAMILIES.transform_values(&:dup)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def set_font(font, size = nil)
|
|
58
|
+
@font = font
|
|
59
|
+
@font_size = size if size
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def save_font
|
|
63
|
+
@font ||= find_font("Helvetica")
|
|
64
|
+
original_font = @font
|
|
65
|
+
original_size = @font_size
|
|
66
|
+
yield
|
|
67
|
+
ensure
|
|
68
|
+
set_font(original_font, original_size) if original_font
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def find_font(name, options = {})
|
|
72
|
+
if font_families.key?(name)
|
|
73
|
+
family = name
|
|
74
|
+
name = font_families[name][options[:style] || :normal]
|
|
75
|
+
if name.is_a?(::Hash)
|
|
76
|
+
options = options.merge(name)
|
|
77
|
+
name = options[:file]
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
key = "#{family}:#{name}:#{options[:font] || 0}:#{Array(options[:features]).join(",")}"
|
|
81
|
+
return font_registry[key] = name if name.is_a?(Prawn::Font)
|
|
82
|
+
|
|
83
|
+
font_registry[key] ||= Font.load(self, name, options.merge(family:))
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def font_registry
|
|
87
|
+
@font_registry ||= {}
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
private
|
|
91
|
+
|
|
92
|
+
def width_of_string(string, options = {})
|
|
93
|
+
font_metric_cache.width_of(string, options)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def font_metric_cache
|
|
97
|
+
@font_metric_cache ||= FontMetricCache.new(self)
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# A font as a document uses it: metrics scaled to the document's current size, and its PDF resource.
|
|
102
|
+
class Font
|
|
103
|
+
attr_reader :name, :family, :options
|
|
104
|
+
|
|
105
|
+
def self.load(document, source, options = {})
|
|
106
|
+
case font_format(source, options)
|
|
107
|
+
when "ttf" then Fonts::TTF.new(document, source, options)
|
|
108
|
+
when "otf" then Fonts::OTF.new(document, source, options)
|
|
109
|
+
when "dfont" then Fonts::DFont.new(document, source, options)
|
|
110
|
+
when "ttc" then Fonts::TTC.new(document, source, options)
|
|
111
|
+
else Fonts::AFM.new(document, source, options)
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def self.font_format(source, options)
|
|
116
|
+
return options.fetch(:format, "ttf") if source.respond_to?(:read)
|
|
117
|
+
|
|
118
|
+
case source.to_s
|
|
119
|
+
when /\.ttf$/i then "ttf"
|
|
120
|
+
when /\.otf$/i then "otf"
|
|
121
|
+
when /\.dfont$/i then "dfont"
|
|
122
|
+
when /\.ttc$/i then "ttc"
|
|
123
|
+
else "afm"
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def initialize(document, name, options = {})
|
|
128
|
+
@document = document
|
|
129
|
+
@name = name
|
|
130
|
+
@options = options
|
|
131
|
+
@family = options[:family]
|
|
132
|
+
@identifier = generate_unique_id
|
|
133
|
+
@references = {}
|
|
134
|
+
@subset_name_cache = {}
|
|
135
|
+
@full_font_embedding = options.key?(:subset) && !options[:subset]
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def ascender
|
|
139
|
+
@ascender / 1000.0 * size
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def descender
|
|
143
|
+
-@descender / 1000.0 * size
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def line_gap
|
|
147
|
+
@line_gap / 1000.0 * size
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def height_at(size)
|
|
151
|
+
@normalized_height ||= (@ascender - @descender + @line_gap) / 1000.0
|
|
152
|
+
@normalized_height * size
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def height
|
|
156
|
+
height_at(size)
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# The string in the font's encoding, only to be measured: TTF reads a UTF-8 string as it is, without a copy.
|
|
160
|
+
def measured_string(string)
|
|
161
|
+
normalize_encoding(string)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def add_to_current_page(subset)
|
|
165
|
+
@references[subset] ||= register(subset)
|
|
166
|
+
@document.page.fonts[identifier_for(subset)] = @references[subset]
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def identifier_for(subset)
|
|
170
|
+
@subset_name_cache[subset] ||= @full_font_embedding ? @identifier.to_sym : :"#{@identifier}.#{subset}"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def inspect
|
|
174
|
+
"#{self.class.name}< #{name}: #{size} >"
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def hash
|
|
178
|
+
[self.class, name, family].hash
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def eql?(other)
|
|
182
|
+
self.class == other.class && name == other.name && family == other.family && size == other.size
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
protected
|
|
186
|
+
|
|
187
|
+
def size
|
|
188
|
+
@document.font_size
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
private
|
|
192
|
+
|
|
193
|
+
def generate_unique_id
|
|
194
|
+
font_count = @document.font_registry.size + 1
|
|
195
|
+
font_count += 1 until @document.page.fonts.keys.none? { _1.to_s.start_with?("F#{font_count}.") }
|
|
196
|
+
:"F#{font_count}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Widths of strings, cached per font, size and options: text layout measures the same words again and again.
|
|
201
|
+
# Nested hashes, so a lookup hashes the string only.
|
|
202
|
+
class FontMetricCache
|
|
203
|
+
def initialize(document)
|
|
204
|
+
@document = document
|
|
205
|
+
@width_by_string_by_settings = Hash.new { |cache, settings| cache[settings] = {} }.compare_by_identity
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def width_of(string, options)
|
|
209
|
+
font = options[:style] ? @document.find_font(@document.font.family, style: options[:style]) : @document.font
|
|
210
|
+
encoded_string = font.measured_string(string)
|
|
211
|
+
width = width_by_string(font, @document.font_size, options[:size], options[:kerning])[encoded_string] ||=
|
|
212
|
+
font.compute_width_of(encoded_string, options)
|
|
213
|
+
character_count = @document.font.character_count(encoded_string)
|
|
214
|
+
character_count.positive? ? width + (@document.character_spacing * (character_count - 1)) : width
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
private
|
|
218
|
+
|
|
219
|
+
# The table of the last settings stays at hand: the words of a paragraph are all measured with the same ones.
|
|
220
|
+
def width_by_string(font, font_size, size, kerning)
|
|
221
|
+
unless font.equal?(@last_font) && font_size.eql?(@last_font_size) && size.eql?(@last_size) && kerning.eql?(@last_kerning)
|
|
222
|
+
@last_font, @last_font_size, @last_size, @last_kerning = font, font_size, size, kerning
|
|
223
|
+
@last_width_by_string = @width_by_string_by_settings[font][[font_size, size, kerning]] ||= {}
|
|
224
|
+
end
|
|
225
|
+
@last_width_by_string
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
end
|