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.
Files changed (160) hide show
  1. checksums.yaml +7 -0
  2. data/COPYING +2 -0
  3. data/GPLv2 +339 -0
  4. data/GPLv3 +674 -0
  5. data/LICENSE +65 -0
  6. data/LICENSE-prawn-svg +21 -0
  7. data/README.md +107 -0
  8. data/data/fonts/Courier-Bold.afm +342 -0
  9. data/data/fonts/Courier-BoldOblique.afm +342 -0
  10. data/data/fonts/Courier-Oblique.afm +342 -0
  11. data/data/fonts/Courier.afm +342 -0
  12. data/data/fonts/Helvetica-Bold.afm +2827 -0
  13. data/data/fonts/Helvetica-BoldOblique.afm +2827 -0
  14. data/data/fonts/Helvetica-Oblique.afm +3051 -0
  15. data/data/fonts/Helvetica.afm +3051 -0
  16. data/data/fonts/MustRead.html +19 -0
  17. data/data/fonts/Symbol.afm +213 -0
  18. data/data/fonts/Times-Bold.afm +2588 -0
  19. data/data/fonts/Times-BoldItalic.afm +2384 -0
  20. data/data/fonts/Times-Italic.afm +2667 -0
  21. data/data/fonts/Times-Roman.afm +2419 -0
  22. data/data/fonts/ZapfDingbats.afm +225 -0
  23. data/lib/pdf/core.rb +4 -0
  24. data/lib/prawn/measurement_extensions.rb +4 -0
  25. data/lib/prawn/svg/attributes/clip_path.rb +46 -0
  26. data/lib/prawn/svg/attributes/mask.rb +16 -0
  27. data/lib/prawn/svg/attributes/opacity.rb +30 -0
  28. data/lib/prawn/svg/attributes/space.rb +10 -0
  29. data/lib/prawn/svg/attributes/stroke.rb +50 -0
  30. data/lib/prawn/svg/attributes/transform.rb +10 -0
  31. data/lib/prawn/svg/attributes.rb +6 -0
  32. data/lib/prawn/svg/calculators/arc_to_bezier_curve.rb +57 -0
  33. data/lib/prawn/svg/calculators/aspect_ratio.rb +60 -0
  34. data/lib/prawn/svg/calculators/document_sizing.rb +103 -0
  35. data/lib/prawn/svg/calculators/path_length.rb +168 -0
  36. data/lib/prawn/svg/calculators/pixels.rb +57 -0
  37. data/lib/prawn/svg/color.rb +240 -0
  38. data/lib/prawn/svg/css/font_face_parser.rb +72 -0
  39. data/lib/prawn/svg/css/font_family_parser.rb +39 -0
  40. data/lib/prawn/svg/css/font_parser.rb +46 -0
  41. data/lib/prawn/svg/css/selector_parser.rb +178 -0
  42. data/lib/prawn/svg/css/stylesheets.rb +233 -0
  43. data/lib/prawn/svg/css/values_parser.rb +68 -0
  44. data/lib/prawn/svg/document.rb +170 -0
  45. data/lib/prawn/svg/elements/anchor.rb +10 -0
  46. data/lib/prawn/svg/elements/base.rb +312 -0
  47. data/lib/prawn/svg/elements/bbox_scaling.rb +78 -0
  48. data/lib/prawn/svg/elements/call_duplicator.rb +37 -0
  49. data/lib/prawn/svg/elements/circle.rb +25 -0
  50. data/lib/prawn/svg/elements/clip_path.rb +26 -0
  51. data/lib/prawn/svg/elements/container.rb +10 -0
  52. data/lib/prawn/svg/elements/direct_render_base.rb +27 -0
  53. data/lib/prawn/svg/elements/ellipse.rb +22 -0
  54. data/lib/prawn/svg/elements/gradient.rb +184 -0
  55. data/lib/prawn/svg/elements/ignored.rb +5 -0
  56. data/lib/prawn/svg/elements/image.rb +177 -0
  57. data/lib/prawn/svg/elements/line.rb +27 -0
  58. data/lib/prawn/svg/elements/marker.rb +72 -0
  59. data/lib/prawn/svg/elements/mask.rb +65 -0
  60. data/lib/prawn/svg/elements/path.rb +294 -0
  61. data/lib/prawn/svg/elements/pattern.rb +150 -0
  62. data/lib/prawn/svg/elements/polygon.rb +25 -0
  63. data/lib/prawn/svg/elements/polyline.rb +23 -0
  64. data/lib/prawn/svg/elements/rect.rb +64 -0
  65. data/lib/prawn/svg/elements/root.rb +22 -0
  66. data/lib/prawn/svg/elements/switch.rb +92 -0
  67. data/lib/prawn/svg/elements/text.rb +99 -0
  68. data/lib/prawn/svg/elements/text_component.rb +346 -0
  69. data/lib/prawn/svg/elements/text_node.rb +308 -0
  70. data/lib/prawn/svg/elements/text_path.rb +153 -0
  71. data/lib/prawn/svg/elements/use.rb +142 -0
  72. data/lib/prawn/svg/elements/viewport.rb +29 -0
  73. data/lib/prawn/svg/elements.rb +46 -0
  74. data/lib/prawn/svg/extension.rb +25 -0
  75. data/lib/prawn/svg/font.rb +20 -0
  76. data/lib/prawn/svg/font_metrics.rb +144 -0
  77. data/lib/prawn/svg/font_registry.rb +315 -0
  78. data/lib/prawn/svg/funciri.rb +14 -0
  79. data/lib/prawn/svg/gradient_renderer.rb +332 -0
  80. data/lib/prawn/svg/gradients.rb +53 -0
  81. data/lib/prawn/svg/interface.rb +72 -0
  82. data/lib/prawn/svg/length.rb +43 -0
  83. data/lib/prawn/svg/link_renderer.rb +40 -0
  84. data/lib/prawn/svg/loaders/data.rb +17 -0
  85. data/lib/prawn/svg/loaders/file.rb +111 -0
  86. data/lib/prawn/svg/loaders/web.rb +24 -0
  87. data/lib/prawn/svg/paint.rb +67 -0
  88. data/lib/prawn/svg/pathable.rb +153 -0
  89. data/lib/prawn/svg/pattern_renderer.rb +112 -0
  90. data/lib/prawn/svg/pdfmatrix.rb +37 -0
  91. data/lib/prawn/svg/percentage.rb +24 -0
  92. data/lib/prawn/svg/properties.rb +329 -0
  93. data/lib/prawn/svg/renderer.rb +249 -0
  94. data/lib/prawn/svg/state.rb +24 -0
  95. data/lib/prawn/svg/transform_parser.rb +78 -0
  96. data/lib/prawn/svg/ttc.rb +35 -0
  97. data/lib/prawn/svg/ttf.rb +95 -0
  98. data/lib/prawn/svg/url_loader.rb +46 -0
  99. data/lib/prawn/svg/version.rb +5 -0
  100. data/lib/prawn/table.rb +4 -0
  101. data/lib/prawn-svg.rb +53 -0
  102. data/lib/prawn.rb +10 -0
  103. data/lib/prawnx/annotations.rb +21 -0
  104. data/lib/prawnx/document/bounding_box.rb +193 -0
  105. data/lib/prawnx/document/column_box.rb +91 -0
  106. data/lib/prawnx/document/internals.rb +144 -0
  107. data/lib/prawnx/document/span.rb +25 -0
  108. data/lib/prawnx/document.rb +253 -0
  109. data/lib/prawnx/encoding.rb +29 -0
  110. data/lib/prawnx/errors.rb +37 -0
  111. data/lib/prawnx/font.rb +214 -0
  112. data/lib/prawnx/fonts/afm.rb +171 -0
  113. data/lib/prawnx/fonts/otf.rb +50 -0
  114. data/lib/prawnx/fonts/ttf.rb +410 -0
  115. data/lib/prawnx/graphic_state.rb +41 -0
  116. data/lib/prawnx/graphics/color.rb +148 -0
  117. data/lib/prawnx/graphics/patterns.rb +100 -0
  118. data/lib/prawnx/graphics/style.rb +171 -0
  119. data/lib/prawnx/graphics.rb +208 -0
  120. data/lib/prawnx/grid.rb +216 -0
  121. data/lib/prawnx/images/image.rb +244 -0
  122. data/lib/prawnx/images/png_decoder.rb +65 -0
  123. data/lib/prawnx/images.rb +115 -0
  124. data/lib/prawnx/measurement_extensions.rb +40 -0
  125. data/lib/prawnx/measurements.rb +62 -0
  126. data/lib/prawnx/outline.rb +146 -0
  127. data/lib/prawnx/page.rb +125 -0
  128. data/lib/prawnx/page_geometry.rb +27 -0
  129. data/lib/prawnx/pdf/outline_node.rb +53 -0
  130. data/lib/prawnx/pdf/reference.rb +31 -0
  131. data/lib/prawnx/pdf/writer.rb +105 -0
  132. data/lib/prawnx/pdf.rb +66 -0
  133. data/lib/prawnx/pdf_a/srgb_profile.rb +61 -0
  134. data/lib/prawnx/pdf_a.rb +130 -0
  135. data/lib/prawnx/pdf_core.rb +25 -0
  136. data/lib/prawnx/repeater.rb +79 -0
  137. data/lib/prawnx/security.rb +108 -0
  138. data/lib/prawnx/stamp.rb +74 -0
  139. data/lib/prawnx/table/cell/kinds.rb +246 -0
  140. data/lib/prawnx/table/cell.rb +331 -0
  141. data/lib/prawnx/table/cells.rb +208 -0
  142. data/lib/prawnx/table.rb +296 -0
  143. data/lib/prawnx/text/box.rb +16 -0
  144. data/lib/prawnx/text/formatted/arranger.rb +217 -0
  145. data/lib/prawnx/text/formatted/box.rb +312 -0
  146. data/lib/prawnx/text/formatted/fragment.rb +205 -0
  147. data/lib/prawnx/text/formatted/line_wrap.rb +204 -0
  148. data/lib/prawnx/text/formatted/parser.rb +149 -0
  149. data/lib/prawnx/text/formatted/wrap.rb +103 -0
  150. data/lib/prawnx/text.rb +281 -0
  151. data/lib/prawnx/transformation_stack.rb +43 -0
  152. data/lib/prawnx/true_type/cff_subset.rb +157 -0
  153. data/lib/prawnx/true_type/file.rb +371 -0
  154. data/lib/prawnx/true_type/open_type_layout.rb +189 -0
  155. data/lib/prawnx/true_type/simple_subsets.rb +94 -0
  156. data/lib/prawnx/true_type/subset.rb +187 -0
  157. data/lib/prawnx/version.rb +7 -0
  158. data/lib/prawnx/view.rb +28 -0
  159. data/lib/prawnx.rb +102 -0
  160. 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
@@ -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