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