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,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
class Document
|
|
5
|
+
# A column of the given width across pages, positioned within the margins, starting at the cursor.
|
|
6
|
+
def span(width, options = {})
|
|
7
|
+
PrawnX.verify_options([:position], options)
|
|
8
|
+
original_position = y
|
|
9
|
+
left_boundary =
|
|
10
|
+
case options.fetch(:position, :left)
|
|
11
|
+
when :left then margin_box.absolute_left
|
|
12
|
+
when :center then margin_box.absolute_left + (margin_box.width / 2.0) - (width / 2.0)
|
|
13
|
+
when :right then margin_box.absolute_right - width
|
|
14
|
+
when Numeric then margin_box.absolute_left + options[:position]
|
|
15
|
+
else raise ArgumentError, "Invalid option for :position"
|
|
16
|
+
end
|
|
17
|
+
canvas do
|
|
18
|
+
bounding_box([left_boundary, margin_box.absolute_top], width:) do
|
|
19
|
+
self.y = original_position
|
|
20
|
+
yield
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
4
|
+
class Document
|
|
5
|
+
include Internals
|
|
6
|
+
include Graphics
|
|
7
|
+
include Text
|
|
8
|
+
include TransformationStack
|
|
9
|
+
include Annotations
|
|
10
|
+
include Destinations
|
|
11
|
+
include Security
|
|
12
|
+
include Images
|
|
13
|
+
include 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
|
+
PrawnX.verify_options(VALID_OPTIONS, options)
|
|
43
|
+
self.class.extensions.reverse_each { extend(it) }
|
|
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) : block.call(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
|
+
page_options[:graphic_state] = new_graphic_state
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
@page = Page.new(self, page_options)
|
|
75
|
+
apply_margin_options(options)
|
|
76
|
+
generate_margin_box
|
|
77
|
+
@bounding_box = @margin_box if last_page && (last_page.size != page.size || last_page.layout != page.layout)
|
|
78
|
+
use_graphic_settings
|
|
79
|
+
return if options[:orphan]
|
|
80
|
+
|
|
81
|
+
insert_page(page, @page_number)
|
|
82
|
+
@page_number += 1
|
|
83
|
+
canvas { image(@background, scale: @background_scale, at: bounds.top_left) } if @background
|
|
84
|
+
@y = @bounding_box.absolute_top
|
|
85
|
+
float { @on_page_create&.call(self) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def delete_page(index)
|
|
89
|
+
return false if index.abs > pages.count - 1
|
|
90
|
+
|
|
91
|
+
pages.delete_at(index)
|
|
92
|
+
pages_reference.data[:Kids].delete_at(index)
|
|
93
|
+
pages_reference.data[:Count] -= 1
|
|
94
|
+
@page_number -= 1
|
|
95
|
+
true
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def page_count
|
|
99
|
+
pages.size
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def go_to_page(page_number)
|
|
103
|
+
@page_number = page_number
|
|
104
|
+
@page = pages[page_number - 1]
|
|
105
|
+
generate_margin_box
|
|
106
|
+
@y = @bounding_box.absolute_top
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def y=(new_y)
|
|
110
|
+
@y = new_y
|
|
111
|
+
bounds.update_height
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def cursor
|
|
115
|
+
y - bounds.absolute_bottom
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def move_cursor_to(new_y)
|
|
119
|
+
self.y = new_y + bounds.absolute_bottom
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def float
|
|
123
|
+
original_page = page_number
|
|
124
|
+
original_y = y
|
|
125
|
+
yield
|
|
126
|
+
go_to_page(original_page) unless page_number == original_page
|
|
127
|
+
self.y = original_y
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def render(*arguments)
|
|
131
|
+
output = arguments.first
|
|
132
|
+
(1..page_count).each do |page_number|
|
|
133
|
+
go_to_page(page_number)
|
|
134
|
+
repeaters.each { it.run(page_number) }
|
|
135
|
+
end
|
|
136
|
+
pdf = render_document
|
|
137
|
+
output << pdf if output.respond_to?(:<<)
|
|
138
|
+
pdf
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def svg(data, options = {}, &)
|
|
142
|
+
require "prawn-svg"
|
|
143
|
+
SVG::Extension.instance_method(:svg).bind_call(self, data, options, &)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def render_file(filename)
|
|
147
|
+
File.binwrite(filename, render)
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def bounds
|
|
151
|
+
@bounding_box
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def bounds=(bounding_box)
|
|
155
|
+
@bounding_box = bounding_box
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
def reference_bounds
|
|
159
|
+
@bounding_box.reference_bounds
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
def move_up(amount)
|
|
163
|
+
self.y += amount
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
def move_down(amount)
|
|
167
|
+
self.y -= amount
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def pad_top(amount)
|
|
171
|
+
move_down(amount)
|
|
172
|
+
yield
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
def pad_bottom(amount)
|
|
176
|
+
yield
|
|
177
|
+
move_down(amount)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def pad(amount)
|
|
181
|
+
move_down(amount)
|
|
182
|
+
yield
|
|
183
|
+
move_down(amount)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def indent(left, right = 0, &)
|
|
187
|
+
bounds.indent(left, right, &)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def page_match?(page_filter, page_number)
|
|
191
|
+
case page_filter
|
|
192
|
+
when :all then true
|
|
193
|
+
when :odd then page_number.odd?
|
|
194
|
+
when :even then page_number.even?
|
|
195
|
+
when Range, Array then page_filter.include?(page_number)
|
|
196
|
+
when Proc then page_filter.call(page_number)
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def group(*)
|
|
201
|
+
raise NotImplementedError, "Document#group has been disabled because its implementation lead to corrupted documents whenever a page boundary was crossed"
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def transaction
|
|
205
|
+
raise NotImplementedError, "Document#transaction has been disabled because its implementation lead to corrupted documents whenever a page boundary was crossed"
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def initialize_first_page(options)
|
|
209
|
+
start_new_page(options[:skip_page_creation] ? options.merge(orphan: true) : options)
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def repeaters
|
|
213
|
+
@repeaters ||= []
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
private
|
|
217
|
+
|
|
218
|
+
def use_graphic_settings(override_settings = false)
|
|
219
|
+
set_fill_color if current_fill_color != "000000" || override_settings
|
|
220
|
+
set_stroke_color if current_stroke_color != "000000" || override_settings
|
|
221
|
+
write_line_width if line_width != 1 || override_settings
|
|
222
|
+
write_stroke_cap_style if cap_style != :butt || override_settings
|
|
223
|
+
write_stroke_join_style if join_style != :miter || override_settings
|
|
224
|
+
write_stroke_dash if dashed? || override_settings
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def generate_margin_box
|
|
228
|
+
old_margin_box = @margin_box
|
|
229
|
+
_left, _bottom, page_width, page_height = page.dimensions
|
|
230
|
+
@margin_box = BoundingBox.new(
|
|
231
|
+
self, nil, [page.margins[:left], page_height - page.margins[:top]],
|
|
232
|
+
width: page_width - (page.margins[:left] + page.margins[:right]),
|
|
233
|
+
height: page_height - (page.margins[:top] + page.margins[:bottom]),
|
|
234
|
+
)
|
|
235
|
+
if old_margin_box
|
|
236
|
+
@margin_box.add_left_padding(old_margin_box.total_left_padding)
|
|
237
|
+
@margin_box.add_right_padding(old_margin_box.total_right_padding)
|
|
238
|
+
end
|
|
239
|
+
@bounding_box = @margin_box unless @bounding_box&.parent
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
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
|
|
243
|
+
|
|
244
|
+
def apply_margin_options(options)
|
|
245
|
+
margin = Array(options[:margin])
|
|
246
|
+
positions = MARGIN_POSITIONS_BY_COUNT[margin.length]
|
|
247
|
+
%i[top right bottom left].zip(positions).each do |side, position|
|
|
248
|
+
new_margin = options[:"#{side}_margin"] || (margin[position] if position)
|
|
249
|
+
page.margins[side] = new_margin if new_margin
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
end
|
|
253
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
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
|
+
*%w[
|
|
10
|
+
space exclam quotedbl numbersign dollar percent ampersand quotesingle parenleft parenright asterisk plus comma
|
|
11
|
+
hyphen period slash zero one two three four five six seven eight nine colon semicolon less equal greater
|
|
12
|
+
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
|
|
13
|
+
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
|
|
14
|
+
asciitilde .notdef Euro .notdef quotesinglbase florin quotedblbase ellipsis dagger daggerdbl circumflex
|
|
15
|
+
perthousand Scaron guilsinglleft OE .notdef Zcaron .notdef .notdef quoteleft quoteright quotedblleft
|
|
16
|
+
quotedblright bullet endash emdash tilde trademark scaron guilsinglright oe .notdef zcaron Ydieresis space
|
|
17
|
+
exclamdown cent sterling currency yen brokenbar section dieresis copyright ordfeminine guillemotleft
|
|
18
|
+
logicalnot hyphen registered macron degree plusminus twosuperior threesuperior acute mu paragraph
|
|
19
|
+
periodcentered cedilla onesuperior ordmasculine guillemotright onequarter onehalf threequarters
|
|
20
|
+
questiondown Agrave Aacute Acircumflex Atilde Adieresis Aring AE Ccedilla Egrave Eacute Ecircumflex
|
|
21
|
+
Edieresis Igrave Iacute Icircumflex Idieresis Eth Ntilde Ograve Oacute Ocircumflex Otilde Odieresis multiply
|
|
22
|
+
Oslash Ugrave Uacute Ucircumflex Udieresis Yacute Thorn germandbls agrave aacute acircumflex atilde adieresis
|
|
23
|
+
aring ae ccedilla egrave eacute ecircumflex edieresis igrave iacute icircumflex idieresis eth ntilde ograve
|
|
24
|
+
oacute ocircumflex otilde odieresis divide oslash ugrave uacute ucircumflex udieresis yacute thorn ydieresis
|
|
25
|
+
],
|
|
26
|
+
].freeze
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
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 PrawnX
|
|
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
|
+
IncompatibleStringEncoding = Class.new(StandardError)
|
|
23
|
+
InvalidGraphicsPath = Class.new(StandardError)
|
|
24
|
+
InvalidJoinStyle = Class.new(StandardError)
|
|
25
|
+
InvalidName = Class.new(StandardError)
|
|
26
|
+
InvalidTableSpan = Class.new(StandardError)
|
|
27
|
+
NameTaken = Class.new(StandardError)
|
|
28
|
+
NotOnPage = Class.new(StandardError)
|
|
29
|
+
RequiredOption = Class.new(StandardError)
|
|
30
|
+
UndefinedObjectName = Class.new(StandardError)
|
|
31
|
+
UnknownFont = Class.new(StandardError)
|
|
32
|
+
UnknownOption = Class.new(StandardError)
|
|
33
|
+
UnknownOutlineTitle = Class.new(StandardError)
|
|
34
|
+
UnrecognizedTableContent = Class.new(StandardError)
|
|
35
|
+
UnsupportedImageType = Class.new(StandardError)
|
|
36
|
+
end
|
|
37
|
+
end
|
data/lib/prawnx/font.rb
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module PrawnX
|
|
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?(PrawnX::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
|
+
def add_to_current_page(subset)
|
|
160
|
+
@references[subset] ||= register(subset)
|
|
161
|
+
@document.page.fonts[identifier_for(subset)] = @references[subset]
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def identifier_for(subset)
|
|
165
|
+
@subset_name_cache[subset] ||= @full_font_embedding ? @identifier.to_sym : :"#{@identifier}.#{subset}"
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
# PrawnX answers to Prawn's name: apps print this, and documents show it.
|
|
169
|
+
def inspect
|
|
170
|
+
"#{self.class.name.sub(/\APrawnX::/, "Prawn::")}< #{name}: #{size} >"
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def hash
|
|
174
|
+
[self.class, name, family].hash
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
def eql?(other)
|
|
178
|
+
self.class == other.class && name == other.name && family == other.family && size == other.size
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
protected
|
|
182
|
+
|
|
183
|
+
def size
|
|
184
|
+
@document.font_size
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
private
|
|
188
|
+
|
|
189
|
+
def generate_unique_id
|
|
190
|
+
font_count = @document.font_registry.size + 1
|
|
191
|
+
font_count += 1 until @document.page.fonts.keys.none? { it.to_s.start_with?("F#{font_count}.") }
|
|
192
|
+
:"F#{font_count}"
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Widths of strings, cached per font, size and options: text layout measures the same words again and again.
|
|
197
|
+
# Nested hashes, so a lookup hashes the string only.
|
|
198
|
+
class FontMetricCache
|
|
199
|
+
def initialize(document)
|
|
200
|
+
@document = document
|
|
201
|
+
@width_by_string_by_settings = Hash.new { |cache, settings| cache[settings] = {} }.compare_by_identity
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def width_of(string, options)
|
|
205
|
+
font = options[:style] ? @document.find_font(@document.font.family, style: options[:style]) : @document.font
|
|
206
|
+
encoded_string = font.normalize_encoding(string)
|
|
207
|
+
by_size = @width_by_string_by_settings[font]
|
|
208
|
+
width_by_string = (by_size[[@document.font_size, options[:size], options[:kerning]]] ||= {})
|
|
209
|
+
width = width_by_string[encoded_string] ||= font.compute_width_of(encoded_string, options)
|
|
210
|
+
character_count = @document.font.character_count(encoded_string)
|
|
211
|
+
character_count.positive? ? width + (@document.character_spacing * (character_count - 1)) : width
|
|
212
|
+
end
|
|
213
|
+
end
|
|
214
|
+
end
|