pdf-core 0.4.0 → 0.10.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.
data/lib/pdf/core/text.rb CHANGED
@@ -1,4 +1,4 @@
1
- # encoding: utf-8
1
+ # frozen_string_literal: true
2
2
 
3
3
  # prawn/core/text.rb : Implements low level text helpers for Prawn
4
4
  #
@@ -8,23 +8,47 @@
8
8
 
9
9
  module PDF
10
10
  module Core
11
- module Text #:nodoc:
11
+ # Low-level text rendering.
12
+ module Text
13
+ # Valid options of text drawing.
12
14
  # These should be used as a base. Extensions may build on this list
13
- #
14
- VALID_OPTIONS = [:kerning, :size, :style]
15
- MODES = { :fill => 0, :stroke => 1, :fill_stroke => 2, :invisible => 3,
16
- :fill_clip => 4, :stroke_clip => 5, :fill_stroke_clip => 6,
17
- :clip => 7 }
15
+ VALID_OPTIONS = %i[kerning size style].freeze
16
+
17
+ # text rendering modes
18
+ MODES = {
19
+ fill: 0,
20
+ stroke: 1,
21
+ fill_stroke: 2,
22
+ invisible: 3,
23
+ fill_clip: 4,
24
+ stroke_clip: 5,
25
+ fill_stroke_clip: 6,
26
+ clip: 7,
27
+ }.freeze
28
+
29
+ # Sygnals that a font doesn't have a name.
30
+ class BadFontFamily < StandardError
31
+ def initialize(message = 'Bad font family')
32
+ super
33
+ end
34
+ end
18
35
 
36
+ # @deprecated
19
37
  attr_reader :skip_encoding
20
38
 
21
- # Low level call to set the current font style and extract text options from
22
- # an options hash. Should be called from within a save_font block
39
+ # Low level call to set the current font style and extract text options
40
+ # from an options hash. Should be called from within a save_font block
23
41
  #
42
+ # @param options [Hash]
43
+ # @option options :style [Symbol, String]
44
+ # @option options :kerning [Boolean]
45
+ # @option options :size [Numeric]
46
+ # @return [void]
24
47
  def process_text_options(options)
25
48
  if options[:style]
26
- raise "Bad font family" unless font.family
27
- font(font.family, :style => options[:style])
49
+ raise BadFontFamily unless font.family
50
+
51
+ font(font.family, style: options[:style])
28
52
  end
29
53
 
30
54
  # must compare against false to keep kerning on as default
@@ -37,26 +61,31 @@ module PDF
37
61
 
38
62
  # Retrieve the current default kerning setting.
39
63
  #
40
- # Defaults to true
64
+ # Defaults to `true`.
41
65
  #
66
+ # @return [Boolean]
42
67
  def default_kerning?
43
- return true if !defined?(@default_kerning)
68
+ return true unless defined?(@default_kerning)
69
+
44
70
  @default_kerning
45
71
  end
46
72
 
47
- # Call with a boolean to set the document-wide kerning setting. This can be
48
- # overridden using the :kerning text option when drawing text or a text
73
+ # Call with a boolean to set the document-wide kerning setting. This can
74
+ # be overridden using the :kerning text option when drawing text or a text
49
75
  # box.
50
76
  #
77
+ # @example
51
78
  # pdf.default_kerning = false
52
- # pdf.text("hello world") # text is not kerned
53
- # pdf.text("hello world", :kerning => true) # text is kerned
79
+ # pdf.text('hello world') # text is not kerned
80
+ # pdf.text('hello world', kerning: true) # text is kerned
54
81
  #
55
- def default_kerning(boolean)
56
- @default_kerning = boolean
82
+ # @param value [Boolean]
83
+ # @return [void]
84
+ def default_kerning(value)
85
+ @default_kerning = value
57
86
  end
58
87
 
59
- alias_method :default_kerning=, :default_kerning
88
+ alias default_kerning= default_kerning
60
89
 
61
90
  # Call with no argument to retrieve the current default leading.
62
91
  #
@@ -64,21 +93,24 @@ module PDF
64
93
  # overridden using the :leading text option when drawing text or a text
65
94
  # box.
66
95
  #
96
+ # @example
67
97
  # pdf.default_leading = 7
68
- # pdf.text("hello world") # a leading of 7 is used
69
- # pdf.text("hello world", :leading => 0) # a leading of 0 is used
98
+ # pdf.text('hello world') # a leading of 7 is used
99
+ # pdf.text('hello world', leading: 0) # a leading of 0 is used
70
100
  #
71
- # Defaults to 0
101
+ # Defaults to 0.
72
102
  #
73
- def default_leading(number=nil)
103
+ # @param number [Numeric]
104
+ # @return [Numeric]
105
+ def default_leading(number = nil)
74
106
  if number.nil?
75
- defined?(@default_leading) && @default_leading || 0
107
+ (defined?(@default_leading) && @default_leading) || 0
76
108
  else
77
109
  @default_leading = number
78
110
  end
79
111
  end
80
112
 
81
- alias_method :default_leading=, :default_leading
113
+ alias default_leading= default_leading
82
114
 
83
115
  # Call with no argument to retrieve the current text direction.
84
116
  #
@@ -86,29 +118,33 @@ module PDF
86
118
  # overridden using the :direction text option when drawing text or a text
87
119
  # box.
88
120
  #
121
+ # @example
89
122
  # pdf.text_direction = :rtl
90
- # pdf.text("hello world") # prints "dlrow olleh"
91
- # pdf.text("hello world", :direction => :ltr) # prints "hello world"
123
+ # pdf.text('hello world') # prints 'dlrow olleh'
124
+ # pdf.text('hello world', direction: :ltr) # prints 'hello world'
92
125
  #
93
126
  # Valid directions are:
94
127
  #
95
- # * :ltr - left-to-right (default)
96
- # * :rtl - right-to-left
128
+ # * `:ltr` -- left-to-right (default)
129
+ # * `:rtl` -- right-to-left
97
130
  #
98
131
  # Side effects:
99
132
  #
100
- # * When printing left-to-right, the default text alignment is :left
101
- # * When printing right-to-left, the default text alignment is :right
133
+ # * When printing left-to-right, the default text alignment is `:left`
134
+ # * When printing right-to-left, the default text alignment is `:right`
102
135
  #
103
- def text_direction(direction=nil)
136
+ # @param direction [:ltr, :rtl]
137
+ # @return [:ltr]
138
+ # @return [:rtl]
139
+ def text_direction(direction = nil)
104
140
  if direction.nil?
105
- defined?(@text_direction) && @text_direction || :ltr
141
+ (defined?(@text_direction) && @text_direction) || :ltr
106
142
  else
107
143
  @text_direction = direction
108
144
  end
109
145
  end
110
146
 
111
- alias_method :text_direction=, :text_direction
147
+ alias text_direction= text_direction
112
148
 
113
149
  # Call with no argument to retrieve the current fallback fonts.
114
150
  #
@@ -118,139 +154,297 @@ module PDF
118
154
  # rendered using the first font that includes the glyph, starting with the
119
155
  # current font and then moving through :fallback_fonts from left to right.
120
156
  #
121
- # Call with an empty array to turn off fallback fonts
122
- #
123
- # file = "#{Prawn::DATADIR}/fonts/gkai00mp.ttf"
124
- # font_families["Kai"] = {
125
- # :normal => { :file => file, :font => "Kai" }
126
- # }
127
- # file = "#{Prawn::DATADIR}/fonts/Action Man.dfont"
128
- # font_families["Action Man"] = {
129
- # :normal => { :file => file, :font => "ActionMan" },
130
- # }
131
- # fallback_fonts ["Times-Roman", "Kai"]
132
- # font "Action Man"
133
- # text "hello ƒ 你好"
134
- # > hello prints in Action Man
135
- # > ƒ prints in Times-Roman
136
- # > 你好 prints in Kai
137
- #
138
- # fallback_fonts [] # clears document-wide fallback fonts
157
+ # Call with an empty array to turn off fallback fonts.
158
+ #
159
+ # @example
160
+ # file = "#{Prawn::DATADIR}/fonts/gkai00mp.ttf"
161
+ # font_families['Kai'] = {
162
+ # normal: { file: file, font: 'Kai' }
163
+ # }
164
+ # file = "#{Prawn::DATADIR}/fonts/Action Man.dfont"
165
+ # font_families['Action Man'] = {
166
+ # normal: { file: file, font: 'ActionMan' },
167
+ # }
168
+ # fallback_fonts ['Times-Roman', 'Kai']
169
+ # font 'Action Man'
170
+ # text 'hello ƒ 你好'
171
+ # # hello prints in Action Man
172
+ # # ƒ prints in Times-Roman
173
+ # # 你好 prints in Kai
174
+ #
175
+ # fallback_fonts [] # clears document-wide fallback fonts
139
176
  #
140
177
  # Side effects:
141
178
  #
142
179
  # * Increased overhead when fallback fonts are declared as each glyph is
143
180
  # checked to see whether it exists in the current font
144
181
  #
145
- def fallback_fonts(fallback_fonts=nil)
182
+ # @param fallback_fonts [Array<String>]
183
+ # @return [Array<String>]
184
+ def fallback_fonts(fallback_fonts = nil)
146
185
  if fallback_fonts.nil?
147
- defined?(@fallback_fonts) && @fallback_fonts || []
186
+ (defined?(@fallback_fonts) && @fallback_fonts) || []
148
187
  else
149
188
  @fallback_fonts = fallback_fonts
150
189
  end
151
190
  end
152
191
 
153
- alias_method :fallback_fonts=, :fallback_fonts
192
+ alias fallback_fonts= fallback_fonts
154
193
 
155
194
  # Call with no argument to retrieve the current text rendering mode.
156
195
  #
157
196
  # Call with a symbol and block to temporarily change the current
158
197
  # text rendering mode.
159
198
  #
199
+ # Valid modes are:
200
+ #
201
+ # * `:fill` - fill text (default)
202
+ # * `:stroke` - stroke text
203
+ # * `:fill_stroke` - fill, then stroke text
204
+ # * `:invisible` - invisible text
205
+ # * `:fill_clip` - fill text then add to path for clipping
206
+ # * `:stroke_clip` - stroke text then add to path for clipping
207
+ # * `:fill_stroke_clip` - fill then stroke text, then add to path for
208
+ # clipping
209
+ # * `:clip` - add text to path for clipping
210
+ #
211
+ # @example
160
212
  # pdf.text_rendering_mode(:stroke) do
161
- # pdf.text("Outlined Text")
213
+ # pdf.text('Outlined Text')
162
214
  # end
163
215
  #
164
- # Valid modes are:
165
- #
166
- # * :fill - fill text (default)
167
- # * :stroke - stroke text
168
- # * :fill_stroke - fill, then stroke text
169
- # * :invisible - invisible text
170
- # * :fill_clip - fill text then add to path for clipping
171
- # * :stroke_clip - stroke text then add to path for clipping
172
- # * :fill_stroke_clip - fill then stroke text, then add to path for clipping
173
- # * :clip - add text to path for clipping
174
- def text_rendering_mode(mode=nil)
175
- return (defined?(@text_rendering_mode) && @text_rendering_mode || :fill) if mode.nil?
216
+ # @param mode [Symbol]
217
+ # @yield Temporariliy set text rendering mode
218
+ # @return [Symbol] if called withouth mode
219
+ # @return [void] otherwise
220
+ def text_rendering_mode(mode = nil, &block)
221
+ if mode.nil?
222
+ return (defined?(@text_rendering_mode) && @text_rendering_mode) || :fill
223
+ end
224
+
176
225
  unless MODES.key?(mode)
177
- raise ArgumentError, "mode must be between one of #{MODES.keys.join(', ')} (#{mode})"
226
+ raise ArgumentError,
227
+ "mode must be between one of #{MODES.keys.join(', ')} (#{mode})"
178
228
  end
179
- original_mode = self.text_rendering_mode
180
229
 
181
- if original_mode == mode
230
+ if text_rendering_mode == mode
182
231
  yield
183
232
  else
184
- @text_rendering_mode = mode
185
- add_content "\n#{MODES[mode]} Tr"
186
- yield
187
- add_content "\n#{MODES[original_mode]} Tr"
188
- @text_rendering_mode = original_mode
233
+ wrap_and_restore_text_rendering_mode(mode, &block)
189
234
  end
190
235
  end
191
236
 
237
+ # Forget previously set text rendering mode.
238
+ #
239
+ # @return [void]
192
240
  def forget_text_rendering_mode!
193
241
  @text_rendering_mode = :unknown
194
242
  end
195
243
 
196
244
  # Increases or decreases the space between characters.
197
245
  # For horizontal text, a positive value will increase the space.
198
- # For veritical text, a positive value will decrease the space.
246
+ # For vertical text, a positive value will decrease the space.
199
247
  #
200
- def character_spacing(amount=nil)
201
- return defined?(@character_spacing) && @character_spacing || 0 if amount.nil?
202
- original_character_spacing = character_spacing
203
- if original_character_spacing == amount
248
+ # Call with no arguments to retrieve current character spacing.
249
+ #
250
+ # @param amount [Numeric]
251
+ # @yield Temporarily set character spacing
252
+ # @return [Numeric] if called without amount
253
+ # @return [void] otherwise
254
+ def character_spacing(amount = nil, &block)
255
+ if amount.nil?
256
+ return (defined?(@character_spacing) && @character_spacing) || 0
257
+ end
258
+
259
+ if character_spacing == amount
204
260
  yield
205
261
  else
206
- @character_spacing = amount
207
- add_content "\n%.3f Tc" % amount
208
- yield
209
- add_content "\n%.3f Tc" % original_character_spacing
210
- @character_spacing = original_character_spacing
262
+ wrap_and_restore_character_spacing(amount, &block)
211
263
  end
212
264
  end
213
265
 
214
266
  # Increases or decreases the space between words.
215
267
  # For horizontal text, a positive value will increase the space.
216
- # For veritical text, a positive value will decrease the space.
268
+ # For vertical text, a positive value will decrease the space.
269
+ #
270
+ # Call with no arguments to retrieve current word spacing.
217
271
  #
218
- def word_spacing(amount=nil)
219
- return defined?(@word_spacing) && @word_spacing || 0 if amount.nil?
220
- original_word_spacing = word_spacing
221
- if original_word_spacing == amount
272
+ # @param amount [Numeric]
273
+ # @yield Temporarily set word spacing
274
+ # @return [Numeric] if called without amount
275
+ # @return [void] otherwise
276
+ def word_spacing(amount = nil, &block)
277
+ return (defined?(@word_spacing) && @word_spacing) || 0 if amount.nil?
278
+
279
+ if word_spacing == amount
222
280
  yield
223
281
  else
224
- @word_spacing = amount
225
- add_content "\n%.3f Tw" % amount
282
+ wrap_and_restore_word_spacing(amount, &block)
283
+ end
284
+ end
285
+
286
+ # Set the horizontal scaling.
287
+ #
288
+ # @param amount [Numeric] the percentage of the normal width.
289
+ # @yield Temporarili set text scaling
290
+ # @return [Numeric] if called with no arguments
291
+ # @return [void] otherwise
292
+ def horizontal_text_scaling(amount = nil, &block)
293
+ if amount.nil?
294
+ return (defined?(@horizontal_text_scaling) && @horizontal_text_scaling) || 100
295
+ end
296
+
297
+ if horizontal_text_scaling == amount
226
298
  yield
227
- add_content "\n%.3f Tw" % original_word_spacing
228
- @word_spacing = original_word_spacing
299
+ else
300
+ wrap_and_restore_horizontal_text_scaling(amount, &block)
229
301
  end
230
302
  end
231
303
 
304
+ # Move the baseline up or down from its default location.
305
+ # Positive values move the baseline up, negative values move it down, and
306
+ # a zero value resets the baseline to its default location.
307
+ #
308
+ # @param amount [Numeric]
309
+ # @yield Temporarily set text rise
310
+ # @return [Numeric] if called with no arguments
311
+ # @return [void] otherwise
312
+ def rise(amount = nil, &block)
313
+ if amount.nil?
314
+ return (defined?(@rise) && @rise) || 0
315
+ end
316
+
317
+ if rise == amount
318
+ yield
319
+ else
320
+ wrap_and_restore_rise(amount, &block)
321
+ end
322
+ end
323
+
324
+ # Add a text object to content stream.
325
+ #
326
+ # @param text [String]
327
+ # @param x [Numeric] horizontal position of the text origin on the page
328
+ # @param y [Numeric] vertical position of the text origin on the page
329
+ # @param options [Hash]
330
+ # @option options :rotate [Numeric] text rotation angle in degrees
331
+ # @option options :kerning [Boolean]
232
332
  def add_text_content(text, x, y, options)
233
- chunks = font.encode_text(text,options)
333
+ chunks = font.encode_text(text, options)
234
334
 
235
- add_content "\nBT"
335
+ add_content("\nBT")
236
336
 
237
337
  if options[:rotate]
238
- rad = options[:rotate].to_f * Math::PI / 180
239
- arr = [ Math.cos(rad), Math.sin(rad), -Math.sin(rad), Math.cos(rad), x, y ]
240
- add_content "%.3f %.3f %.3f %.3f %.3f %.3f Tm" % arr
338
+ rad = Float(options[:rotate]) * Math::PI / 180
339
+ array = [
340
+ Math.cos(rad),
341
+ Math.sin(rad),
342
+ -Math.sin(rad),
343
+ Math.cos(rad),
344
+ x, y,
345
+ ]
346
+ add_content("#{PDF::Core.real_params(array)} Tm")
241
347
  else
242
- add_content "#{x} #{y} Td"
348
+ add_content("#{PDF::Core.real(x)} #{PDF::Core.real(y)} Td")
243
349
  end
244
350
 
245
351
  chunks.each do |(subset, string)|
246
352
  font.add_to_current_page(subset)
247
- add_content "/#{font.identifier_for(subset)} #{font_size} Tf"
353
+ add_content(
354
+ [
355
+ PDF::Core.pdf_object(font.identifier_for(subset), true),
356
+ PDF::Core.pdf_object(font_size, true),
357
+ 'Tf',
358
+ ].join(' '),
359
+ )
360
+
361
+ operation = options[:kerning] && string.is_a?(Array) ? 'TJ' : 'Tj'
362
+ add_content("#{PDF::Core.pdf_object(string, true)} #{operation}")
363
+ end
364
+
365
+ add_content("ET\n")
366
+ end
367
+
368
+ private
248
369
 
249
- operation = options[:kerning] && string.is_a?(Array) ? "TJ" : "Tj"
250
- add_content PDF::Core::PdfObject(string, true) << " " << operation
370
+ def wrap_and_restore_text_rendering_mode(block_value)
371
+ original_value = text_rendering_mode
372
+ @text_rendering_mode = block_value
373
+ update_text_rendering_mode_state
374
+ begin
375
+ yield
376
+ ensure
377
+ @text_rendering_mode = original_value
378
+ update_text_rendering_mode_state
251
379
  end
380
+ end
381
+
382
+ def update_text_rendering_mode_state
383
+ add_content("\n#{MODES[text_rendering_mode]} Tr")
384
+ end
385
+
386
+ def wrap_and_restore_character_spacing(block_value)
387
+ original_value = character_spacing
388
+ @character_spacing = block_value
389
+ update_character_spacing_state
390
+ begin
391
+ yield
392
+ ensure
393
+ @character_spacing = original_value
394
+ update_character_spacing_state
395
+ end
396
+ end
397
+
398
+ def update_character_spacing_state
399
+ add_content("\n#{PDF::Core.real(character_spacing)} Tc")
400
+ end
401
+
402
+ def wrap_and_restore_word_spacing(block_value)
403
+ original_value = word_spacing
404
+ @word_spacing = block_value
405
+ update_word_spacing_state
406
+ begin
407
+ yield
408
+ ensure
409
+ @word_spacing = original_value
410
+ update_word_spacing_state
411
+ end
412
+ end
413
+
414
+ def update_word_spacing_state
415
+ add_content("\n#{PDF::Core.real(word_spacing)} Tw")
416
+ end
417
+
418
+ def wrap_and_restore_horizontal_text_scaling(block_value)
419
+ original_value = horizontal_text_scaling
420
+ @horizontal_text_scaling = block_value
421
+ update_horizontal_text_scaling_state
422
+ begin
423
+ yield
424
+ ensure
425
+ @horizontal_text_scaling = original_value
426
+ update_horizontal_text_scaling_state
427
+ end
428
+ end
429
+
430
+ def update_horizontal_text_scaling_state
431
+ add_content("\n#{PDF::Core.real(horizontal_text_scaling)} Tz")
432
+ end
433
+
434
+ def wrap_and_restore_rise(block_value)
435
+ original_value = rise
436
+ @rise = block_value
437
+ update_rise_state
438
+ begin
439
+ yield
440
+ ensure
441
+ @rise = original_value
442
+ update_rise_state
443
+ end
444
+ end
252
445
 
253
- add_content "ET\n"
446
+ def update_rise_state
447
+ add_content("\n#{PDF::Core.real(rise)} Ts")
254
448
  end
255
449
  end
256
450
  end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module PDF
4
+ module Core
5
+ # Utility methods
6
+ module Utils
7
+ module_function
8
+
9
+ # Deep clone an object.
10
+ # It uses marshal-demarshal trick. Since it's supposed to be use only on
11
+ # objects that can be serialized into PDF it shouldn't have any issues
12
+ # with objects that can not be marshaled.
13
+ #
14
+ # @param object [any]
15
+ # @return [any]
16
+ def deep_clone(object)
17
+ Marshal.load(Marshal.dump(object))
18
+ end
19
+ end
20
+ end
21
+ end
data/lib/pdf/core.rb CHANGED
@@ -1,34 +1,47 @@
1
- require_relative "core/pdf_object"
2
- require_relative "core/annotations"
3
- require_relative "core/byte_string"
4
- require_relative "core/destinations"
5
- require_relative "core/filters"
6
- require_relative "core/stream"
7
- require_relative "core/reference"
8
- require_relative "core/literal_string"
9
- require_relative "core/filter_list"
10
- require_relative "core/page"
11
- require_relative "core/object_store"
12
- require_relative "core/document_state"
13
- require_relative "core/name_tree"
14
- require_relative "core/graphics_state"
15
- require_relative "core/page_geometry"
16
- require_relative "core/outline_root"
17
- require_relative "core/outline_item"
18
- require_relative "core/renderer"
1
+ # frozen_string_literal: true
19
2
 
3
+ # Top level Module
20
4
  module PDF
5
+ # PDF::Core is concerned with low-level PDF functions such as serialization,
6
+ # content streams and such.
7
+ #
8
+ # It's extracted from Prawn but at the moment is not entirely independent.
21
9
  module Core
10
+ # PDF::Core-specific errors
22
11
  module Errors
23
- # This error is raised when PdfObject() fails
24
- FailedObjectConversion = Class.new(StandardError)
12
+ # This error indicates failure of {PDF::Core.pdf_object}
13
+ class FailedObjectConversion < StandardError
14
+ end
25
15
 
26
- # This error is raise when trying to restore a graphic state that
27
- EmptyGraphicStateStack = Class.new(StandardError)
16
+ # This error occurs when a graphic state is being restored but the graphic
17
+ # state stack is empty.
18
+ class EmptyGraphicStateStack < StandardError
19
+ end
28
20
 
29
- # This error is raised when Document#page_layout is set to anything
30
- # other than :portrait or :landscape
31
- InvalidPageLayout = Class.new(StandardError)
21
+ # This error is raised when page layout is set to anything other than
22
+ # `:portrait` or `:landscape`
23
+ class InvalidPageLayout < StandardError
24
+ end
32
25
  end
33
26
  end
34
27
  end
28
+
29
+ require_relative 'core/pdf_object'
30
+ require_relative 'core/annotations'
31
+ require_relative 'core/byte_string'
32
+ require_relative 'core/destinations'
33
+ require_relative 'core/filters'
34
+ require_relative 'core/stream'
35
+ require_relative 'core/reference'
36
+ require_relative 'core/literal_string'
37
+ require_relative 'core/filter_list'
38
+ require_relative 'core/page'
39
+ require_relative 'core/object_store'
40
+ require_relative 'core/document_state'
41
+ require_relative 'core/name_tree'
42
+ require_relative 'core/graphics_state'
43
+ require_relative 'core/page_geometry'
44
+ require_relative 'core/outline_root'
45
+ require_relative 'core/outline_item'
46
+ require_relative 'core/renderer'
47
+ require_relative 'core/text'