hexapdf 0.20.3 → 0.21.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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/README.md +5 -3
  4. data/Rakefile +10 -1
  5. data/examples/018-composer.rb +10 -10
  6. data/examples/020-column_box.rb +57 -0
  7. data/lib/hexapdf/cli/batch.rb +4 -6
  8. data/lib/hexapdf/cli/info.rb +5 -1
  9. data/lib/hexapdf/cli/inspect.rb +59 -0
  10. data/lib/hexapdf/cli/split.rb +1 -1
  11. data/lib/hexapdf/composer.rb +147 -53
  12. data/lib/hexapdf/configuration.rb +7 -3
  13. data/lib/hexapdf/content/canvas.rb +1 -1
  14. data/lib/hexapdf/content/color_space.rb +1 -1
  15. data/lib/hexapdf/content/operator.rb +7 -7
  16. data/lib/hexapdf/content/parser.rb +3 -3
  17. data/lib/hexapdf/content/processor.rb +9 -9
  18. data/lib/hexapdf/document/signatures.rb +5 -4
  19. data/lib/hexapdf/document.rb +7 -0
  20. data/lib/hexapdf/encryption/aes.rb +9 -5
  21. data/lib/hexapdf/font/true_type/font.rb +7 -7
  22. data/lib/hexapdf/font/true_type/optimizer.rb +1 -1
  23. data/lib/hexapdf/font/true_type/subsetter.rb +1 -1
  24. data/lib/hexapdf/font/true_type/table/cmap_subtable.rb +3 -3
  25. data/lib/hexapdf/font/true_type_wrapper.rb +9 -14
  26. data/lib/hexapdf/font/type1/font.rb +10 -12
  27. data/lib/hexapdf/font/type1_wrapper.rb +15 -17
  28. data/lib/hexapdf/layout/box.rb +12 -9
  29. data/lib/hexapdf/layout/column_box.rb +168 -0
  30. data/lib/hexapdf/layout/image_box.rb +1 -1
  31. data/lib/hexapdf/layout/style.rb +28 -8
  32. data/lib/hexapdf/layout/text_fragment.rb +10 -9
  33. data/lib/hexapdf/parser.rb +5 -0
  34. data/lib/hexapdf/tokenizer.rb +3 -3
  35. data/lib/hexapdf/type/acro_form/appearance_generator.rb +6 -4
  36. data/lib/hexapdf/type/acro_form/choice_field.rb +2 -2
  37. data/lib/hexapdf/type/acro_form/field.rb +2 -2
  38. data/lib/hexapdf/type/font_type0.rb +1 -1
  39. data/lib/hexapdf/type/font_type3.rb +1 -1
  40. data/lib/hexapdf/type/resources.rb +4 -4
  41. data/lib/hexapdf/type/signature/adbe_pkcs7_detached.rb +1 -1
  42. data/lib/hexapdf/type/signature.rb +1 -1
  43. data/lib/hexapdf/type/trailer.rb +3 -3
  44. data/lib/hexapdf/version.rb +1 -1
  45. data/lib/hexapdf/xref_section.rb +1 -1
  46. data/test/hexapdf/common_tokenizer_tests.rb +5 -5
  47. data/test/hexapdf/content/test_graphics_state.rb +1 -0
  48. data/test/hexapdf/content/test_operator.rb +2 -2
  49. data/test/hexapdf/content/test_processor.rb +1 -1
  50. data/test/hexapdf/encryption/test_aes.rb +8 -0
  51. data/test/hexapdf/encryption/test_standard_security_handler.rb +23 -29
  52. data/test/hexapdf/filter/test_predictor.rb +16 -20
  53. data/test/hexapdf/font/test_type1_wrapper.rb +5 -1
  54. data/test/hexapdf/font/true_type/table/common.rb +1 -1
  55. data/test/hexapdf/font/true_type/table/test_cmap.rb +1 -1
  56. data/test/hexapdf/font/true_type/table/test_cmap_subtable.rb +1 -1
  57. data/test/hexapdf/image_loader/test_pdf.rb +6 -8
  58. data/test/hexapdf/image_loader/test_png.rb +2 -2
  59. data/test/hexapdf/layout/test_box.rb +11 -1
  60. data/test/hexapdf/layout/test_style.rb +23 -0
  61. data/test/hexapdf/layout/test_text_fragment.rb +21 -21
  62. data/test/hexapdf/test_composer.rb +115 -52
  63. data/test/hexapdf/test_dictionary.rb +2 -2
  64. data/test/hexapdf/test_document.rb +11 -9
  65. data/test/hexapdf/test_object.rb +1 -1
  66. data/test/hexapdf/test_parser.rb +13 -7
  67. data/test/hexapdf/test_serializer.rb +20 -22
  68. data/test/hexapdf/test_stream.rb +7 -9
  69. data/test/hexapdf/test_writer.rb +2 -2
  70. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +1 -2
  71. data/test/hexapdf/type/acro_form/test_choice_field.rb +1 -1
  72. data/test/hexapdf/type/signature/common.rb +1 -1
  73. data/test/hexapdf/type/test_font_type0.rb +1 -1
  74. data/test/hexapdf/type/test_font_type1.rb +7 -7
  75. data/test/hexapdf/type/test_image.rb +13 -17
  76. metadata +4 -2
@@ -2015,7 +2015,7 @@ module HexaPDF
2015
2015
  invoke2(:Td, offset[0], offset[1])
2016
2016
  end
2017
2017
  else
2018
- invoke0(:"T*")
2018
+ invoke0(:'T*')
2019
2019
  end
2020
2020
  self
2021
2021
  end
@@ -296,7 +296,7 @@ module HexaPDF
296
296
  spec = if first_item.match?(/\A\h{6}\z/)
297
297
  first_item.scan(/../).map!(&:hex)
298
298
  elsif first_item.match?(/\A\h{3}\z/)
299
- first_item.each_char.map {|x| (x*2).hex}
299
+ first_item.each_char.map {|x| (x * 2).hex }
300
300
  elsif CSS_COLOR_NAMES.key?(first_item)
301
301
  CSS_COLOR_NAMES[first_item]
302
302
  else
@@ -1036,14 +1036,14 @@ module HexaPDF
1036
1036
  s: EndPath.new('s'),
1037
1037
  f: EndPath.new('f'),
1038
1038
  F: EndPath.new('F'),
1039
- 'f*'.to_sym => EndPath.new('f*'),
1039
+ 'f*': EndPath.new('f*'),
1040
1040
  B: EndPath.new('B'),
1041
- 'B*'.to_sym => EndPath.new('B*'),
1041
+ 'B*': EndPath.new('B*'),
1042
1042
  b: EndPath.new('b'),
1043
- 'b*'.to_sym => EndPath.new('b*'),
1043
+ 'b*': EndPath.new('b*'),
1044
1044
  n: EndPath.new('n'),
1045
1045
  W: ClipPath.new('W'),
1046
- 'W*'.to_sym => ClipPath.new('W*'),
1046
+ 'W*': ClipPath.new('W*'),
1047
1047
 
1048
1048
  BI: InlineImage.new,
1049
1049
 
@@ -1059,10 +1059,10 @@ module HexaPDF
1059
1059
  Td: MoveText.new,
1060
1060
  TD: MoveTextAndSetLeading.new,
1061
1061
  Tm: SetTextMatrix.new,
1062
- 'T*'.to_sym => MoveTextNextLine.new,
1062
+ 'T*': MoveTextNextLine.new,
1063
1063
  Tj: ShowText.new,
1064
- '\''.to_sym => MoveTextNextLineAndShowText.new,
1065
- '"'.to_sym => SetSpacingMoveTextNextLineAndShowText.new,
1064
+ "'": MoveTextNextLineAndShowText.new,
1065
+ '"': SetSpacingMoveTextNextLineAndShowText.new,
1066
1066
  TJ: ShowTextWithPositioning.new,
1067
1067
  }
1068
1068
  DEFAULT_OPERATORS.default_proc = proc {|h, k| h[k] = BaseOperator.new(k.to_s) }
@@ -94,11 +94,11 @@ module HexaPDF
94
94
  elsif byte == 40 # (
95
95
  parse_literal_string
96
96
  elsif byte == 60 # <
97
- if @string.getbyte(@ss.pos + 1) != 60
98
- parse_hex_string
99
- else
97
+ if @string.getbyte(@ss.pos + 1) == 60
100
98
  @ss.pos += 2
101
99
  TOKEN_DICT_START
100
+ else
101
+ parse_hex_string
102
102
  end
103
103
  elsif byte == 62 # >
104
104
  unless @string.getbyte(@ss.pos + 1) == 62
@@ -187,7 +187,7 @@ module HexaPDF
187
187
 
188
188
  # Returns the concatenated text of the boxes.
189
189
  def string
190
- @boxes.map(&:string).join('')
190
+ @boxes.map(&:string).join
191
191
  end
192
192
 
193
193
  # :call-seq:
@@ -260,14 +260,14 @@ module HexaPDF
260
260
  s: :close_and_stroke_path,
261
261
  f: :fill_path_non_zero,
262
262
  F: :fill_path_non_zero,
263
- 'f*'.to_sym => :fill_path_even_odd,
263
+ 'f*': :fill_path_even_odd,
264
264
  B: :fill_and_stroke_path_non_zero,
265
- 'B*'.to_sym => :fill_and_stroke_path_even_odd,
265
+ 'B*': :fill_and_stroke_path_even_odd,
266
266
  b: :close_fill_and_stroke_path_non_zero,
267
- 'b*'.to_sym => :close_fill_and_stroke_path_even_odd,
267
+ 'b*': :close_fill_and_stroke_path_even_odd,
268
268
  n: :end_path,
269
269
  W: :clip_path_non_zero,
270
- 'W*'.to_sym => :clip_path_even_odd,
270
+ 'W*': :clip_path_even_odd,
271
271
  BT: :begin_text,
272
272
  ET: :end_text,
273
273
  Tc: :set_character_spacing,
@@ -280,10 +280,10 @@ module HexaPDF
280
280
  Td: :move_text,
281
281
  TD: :move_text_and_set_leading,
282
282
  Tm: :set_text_matrix,
283
- 'T*'.to_sym => :move_text_next_line,
283
+ 'T*': :move_text_next_line,
284
284
  Tj: :show_text,
285
- '\''.to_sym => :move_text_next_line_and_show_text,
286
- '"'.to_sym => :set_spacing_move_text_next_line_and_show_text,
285
+ "'": :move_text_next_line_and_show_text,
286
+ '"': :set_spacing_move_text_next_line_and_show_text,
287
287
  TJ: :show_text_with_positioning,
288
288
  d0: :set_glyph_width, # only for Type 3 fonts
289
289
  d1: :set_glyph_width_and_bounding_box, # only for Type 3 fonts
@@ -392,7 +392,7 @@ module HexaPDF
392
392
  data = data.each_with_object(''.b) {|obj, result| result << obj if obj.kind_of?(String) }
393
393
  end
394
394
  font = graphics_state.font
395
- font.decode(data).map {|code_point| font.to_utf8(code_point) }.join('')
395
+ font.decode(data).map {|code_point| font.to_utf8(code_point) }.join
396
396
  end
397
397
 
398
398
  # Decodes the given text object and returns it as a CompositeBox object.
@@ -93,12 +93,12 @@ module HexaPDF
93
93
 
94
94
  # Returns the name to be set on the /Filter key when using this signing handler.
95
95
  def filter_name
96
- :"Adobe.PPKLite"
96
+ :'Adobe.PPKLite'
97
97
  end
98
98
 
99
99
  # Returns the name to be set on the /SubFilter key when using this signing handler.
100
100
  def sub_filter_name
101
- :"adbe.pkcs7.detached"
101
+ :'adbe.pkcs7.detached'
102
102
  end
103
103
 
104
104
  # Sets the DocMDP permissions that should be applied to the document.
@@ -171,7 +171,7 @@ module HexaPDF
171
171
  # Creates a signing handler with the given options and returns it.
172
172
  #
173
173
  # A signing handler name is mapped to a class via the 'signature.signing_handler'
174
- # configuration option.
174
+ # configuration option. The default signing handler is DefaultHandler.
175
175
  def handler(name: :default, **options)
176
176
  handler = @document.config.constantize('signature.signing_handler', name) do
177
177
  raise HexaPDF::Error, "No signing handler named '#{name}' is available"
@@ -183,7 +183,8 @@ module HexaPDF
183
183
  #
184
184
  # This method will add a new signature to the document and write the updated document to the
185
185
  # given file or IO stream. Afterwards the document can't be modified anymore and still retain
186
- # a correct digital signature; create a new document based on the file or IO stream instead.
186
+ # a correct digital signature. To modify the signed document (e.g. for adding another
187
+ # signature) create a new document based on the given file or IO stream instead.
187
188
  #
188
189
  # +signature+::
189
190
  # Can either be a signature object (determined via the /Type key), a signature field or
@@ -638,6 +638,9 @@ module HexaPDF
638
638
  # of the keyword arguments (see HexaPDF::Document::Signatures#handler for details).
639
639
  #
640
640
  # If not changed, the default signing handler is HexaPDF::Document::Signatures::DefaultHandler.
641
+ #
642
+ # *Note*: Once signing is done the document cannot be changed anymore since it was written. If a
643
+ # document needs to be signed multiple times, it needs to be loaded again after writing.
641
644
  def sign(file_or_io, handler: :default, signature: nil, write_options: {}, **handler_options)
642
645
  handler = signatures.handler(name: handler, **handler_options)
643
646
  signatures.add(file_or_io, handler, signature: signature, write_options: write_options)
@@ -714,6 +717,10 @@ module HexaPDF
714
717
  end
715
718
  end
716
719
 
720
+ def inspect #:nodoc:
721
+ "<#{self.class.name}:#{object_id}>"
722
+ end
723
+
717
724
  end
718
725
 
719
726
  end
@@ -114,10 +114,12 @@ module HexaPDF
114
114
  #
115
115
  # See: PDF1.7 s7.6.2.
116
116
  def decrypt(key, data)
117
- if data.length % BLOCK_SIZE != 0 || data.length < 2 * BLOCK_SIZE
117
+ if data.length % BLOCK_SIZE != 0 || data.length < BLOCK_SIZE
118
118
  raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes"
119
119
  end
120
- unpad(new(key, data.slice!(0, BLOCK_SIZE), :decrypt).process(data))
120
+ iv = data.slice!(0, BLOCK_SIZE)
121
+ # Handle invalid files with missing padding
122
+ data.empty? ? data : unpad(new(key, iv, :decrypt).process(data))
121
123
  end
122
124
 
123
125
  # Returns a Fiber object that decrypts the data from the given source fiber with the
@@ -140,11 +142,13 @@ module HexaPDF
140
142
  Fiber.yield(algorithm.process(new_data))
141
143
  end
142
144
 
143
- if data.length < BLOCK_SIZE || data.length % BLOCK_SIZE != 0
145
+ if data.length % BLOCK_SIZE != 0
144
146
  raise HexaPDF::EncryptionError, "Invalid data for decryption, need 32 + 16*n bytes"
147
+ elsif data.empty?
148
+ data # Handle invalid files with missing padding
149
+ else
150
+ unpad(algorithm.process(data))
145
151
  end
146
-
147
- unpad(algorithm.process(data))
148
152
  end
149
153
  end
150
154
 
@@ -123,7 +123,7 @@ module HexaPDF
123
123
 
124
124
  # Returns the weight of the font.
125
125
  def weight
126
- self[:"OS/2"].weight_class || 0
126
+ self[:'OS/2'].weight_class || 0
127
127
  end
128
128
 
129
129
  # Returns the bounding of the font.
@@ -133,22 +133,22 @@ module HexaPDF
133
133
 
134
134
  # Returns the cap height of the font.
135
135
  def cap_height
136
- self[:"OS/2"].cap_height
136
+ self[:'OS/2'].cap_height
137
137
  end
138
138
 
139
139
  # Returns the x-height of the font.
140
140
  def x_height
141
- self[:"OS/2"].x_height
141
+ self[:'OS/2'].x_height
142
142
  end
143
143
 
144
144
  # Returns the ascender of the font.
145
145
  def ascender
146
- self[:"OS/2"].typo_ascender || self[:hhea].ascent
146
+ self[:'OS/2'].typo_ascender || self[:hhea].ascent
147
147
  end
148
148
 
149
149
  # Returns the descender of the font.
150
150
  def descender
151
- self[:"OS/2"].typo_descender || self[:hhea].descent
151
+ self[:'OS/2'].typo_descender || self[:hhea].descent
152
152
  end
153
153
 
154
154
  # Returns the italic angle of the font, in degrees counter-clockwise from the vertical.
@@ -176,12 +176,12 @@ module HexaPDF
176
176
 
177
177
  # Returns the distance from the baseline to the top of the strikeout line.
178
178
  def strikeout_position
179
- self[:"OS/2"].strikeout_position
179
+ self[:'OS/2'].strikeout_position
180
180
  end
181
181
 
182
182
  # Returns the stroke width for the strikeout line.
183
183
  def strikeout_thickness
184
- self[:"OS/2"].strikeout_size
184
+ self[:'OS/2'].strikeout_size
185
185
  end
186
186
 
187
187
  # Returns th glyph ID of the missing glyph, i.e. 0.
@@ -55,7 +55,7 @@ module HexaPDF
55
55
  'hmtx' => font[:hmtx].raw_data,
56
56
  }
57
57
  tables['cmap'] = font[:cmap].raw_data if font[:cmap]
58
- tables['cvt '] = font[:"cvt "].raw_data if font[:"cvt "]
58
+ tables['cvt '] = font[:'cvt '].raw_data if font[:'cvt ']
59
59
  tables['fpgm'] = font[:fpgm].raw_data if font[:fpgm]
60
60
  tables['prep'] = font[:prep].raw_data if font[:prep]
61
61
  Builder.build(tables)
@@ -99,7 +99,7 @@ module HexaPDF
99
99
  'loca' => loca,
100
100
  'hmtx' => hmtx,
101
101
  }
102
- tables['cvt '] = @font[:"cvt "].raw_data if @font[:"cvt "]
102
+ tables['cvt '] = @font[:'cvt '].raw_data if @font[:'cvt ']
103
103
  tables['fpgm'] = @font[:fpgm].raw_data if @font[:fpgm]
104
104
  tables['prep'] = @font[:prep].raw_data if @font[:prep]
105
105
 
@@ -282,12 +282,12 @@ module HexaPDF
282
282
  def self.mapper(end_codes, start_codes, id_deltas, id_range_offsets, glyph_indexes)
283
283
  compute_glyph_id = lambda do |index, code|
284
284
  offset = id_range_offsets[index]
285
- if offset != 0
285
+ if offset == 0
286
+ glyph_id = (code + id_deltas[index]) % 65536
287
+ else
286
288
  glyph_id = glyph_indexes[offset - end_codes.length + (code - start_codes[index])]
287
289
  glyph_id ||= 0 # Handle invalid subtable entries
288
290
  glyph_id = (glyph_id + id_deltas[index]) % 65536 if glyph_id != 0
289
- else
290
- glyph_id = (code + id_deltas[index]) % 65536
291
291
  end
292
292
  glyph_id
293
293
  end
@@ -126,7 +126,6 @@ module HexaPDF
126
126
  # If +subset+ is true, the font is subset.
127
127
  def initialize(document, font, pdf_object: nil, subset: true)
128
128
  @wrapped_font = font
129
- @missing_glyph_callable = document.config['font.on_missing_glyph']
130
129
 
131
130
  @subsetter = (subset ? HexaPDF::Font::TrueType::Subsetter.new(font) : nil)
132
131
 
@@ -165,12 +164,10 @@ module HexaPDF
165
164
  # Note: Although this method is public, it should normally not be used by application code!
166
165
  def glyph(id, str = nil)
167
166
  @id_to_glyph[id] ||=
168
- begin
169
- if id >= 0 && id < @wrapped_font[:maxp].num_glyphs
170
- Glyph.new(@wrapped_font, id, str || (+'' << (@cmap.gid_to_code(id) || 0xFFFD)))
171
- else
172
- @missing_glyph_callable.call("\u{FFFD}", font_type, @wrapped_font)
173
- end
167
+ if id >= 0 && id < @wrapped_font[:maxp].num_glyphs
168
+ Glyph.new(@wrapped_font, id, str || (+'' << (@cmap.gid_to_code(id) || 0xFFFD)))
169
+ else
170
+ @pdf_object.document.config['font.on_missing_glyph'].call("\u{FFFD}", self)
174
171
  end
175
172
  end
176
173
 
@@ -178,12 +175,10 @@ module HexaPDF
178
175
  def decode_utf8(str)
179
176
  str.codepoints.map! do |c|
180
177
  @codepoint_to_glyph[c] ||=
181
- begin
182
- if (gid = @cmap[c])
183
- glyph(gid, +'' << c)
184
- else
185
- @missing_glyph_callable.call(+'' << c, font_type, @wrapped_font)
186
- end
178
+ if (gid = @cmap[c])
179
+ glyph(gid, +'' << c)
180
+ else
181
+ @pdf_object.document.config['font.on_missing_glyph'].call(+'' << c, self)
187
182
  end
188
183
  end
189
184
  end
@@ -250,7 +245,7 @@ module HexaPDF
250
245
  Supplement: 0},
251
246
  CIDToGIDMap: :Identity})
252
247
  dict = document.add({Type: :Font, Subtype: :Type0, BaseFont: cid_font[:BaseFont],
253
- Encoding: :"Identity-H", DescendantFonts: [cid_font]})
248
+ Encoding: :'Identity-H', DescendantFonts: [cid_font]})
254
249
  dict.font_wrapper = self
255
250
 
256
251
  document.register_listener(:complete_objects) do
@@ -89,19 +89,17 @@ module HexaPDF
89
89
  # Returns the built-in encoding of the font.
90
90
  def encoding
91
91
  @encoding ||=
92
- begin
93
- if @metrics.encoding_scheme == 'AdobeStandardEncoding'
94
- Encoding.for_name(:StandardEncoding)
95
- elsif font_name == 'ZapfDingbats' || font_name == 'Symbol'
96
- Encoding.for_name((font_name + "Encoding").to_sym)
97
- else
98
- encoding = Encoding::Base.new
99
- @metrics.character_metrics.each do |key, char_metric|
100
- next unless key.kind_of?(Integer) && key >= 0
101
- encoding.code_to_name[key] = char_metric.name
102
- end
103
- encoding
92
+ if @metrics.encoding_scheme == 'AdobeStandardEncoding'
93
+ Encoding.for_name(:StandardEncoding)
94
+ elsif font_name == 'ZapfDingbats' || font_name == 'Symbol'
95
+ Encoding.for_name("#{font_name}Encoding".to_sym)
96
+ else
97
+ encoding = Encoding::Base.new
98
+ @metrics.character_metrics.each do |key, char_metric|
99
+ next unless key.kind_of?(Integer) && key >= 0
100
+ encoding.code_to_name[key] = char_metric.name
104
101
  end
102
+ encoding
105
103
  end
106
104
  end
107
105
 
@@ -119,7 +119,6 @@ module HexaPDF
119
119
  def initialize(document, font, pdf_object: nil, custom_encoding: false)
120
120
  @wrapped_font = font
121
121
  @pdf_object = pdf_object || create_pdf_object(document)
122
- @missing_glyph_callable = document.config['font.on_missing_glyph']
123
122
 
124
123
  if pdf_object
125
124
  @encoding = pdf_object.encoding
@@ -160,7 +159,7 @@ module HexaPDF
160
159
  if @wrapped_font.metrics.character_metrics.key?(name)
161
160
  Glyph.new(@wrapped_font, name, str)
162
161
  else
163
- @missing_glyph_callable.call(str, font_type, @wrapped_font)
162
+ @pdf_object.document.config['font.on_missing_glyph'].call(str, self)
164
163
  end
165
164
  end
166
165
  end
@@ -212,24 +211,23 @@ module HexaPDF
212
211
 
213
212
  # Creates a PDF object representing the wrapped font for the given PDF document.
214
213
  def create_pdf_object(document)
215
- fd = document.wrap({Type: :FontDescriptor,
216
- FontName: @wrapped_font.font_name.intern,
217
- FontWeight: @wrapped_font.weight_class,
218
- FontBBox: @wrapped_font.bounding_box,
219
- ItalicAngle: @wrapped_font.italic_angle || 0,
220
- Ascent: @wrapped_font.ascender || 0,
221
- Descent: @wrapped_font.descender || 0,
222
- CapHeight: @wrapped_font.cap_height,
223
- XHeight: @wrapped_font.x_height,
224
- StemH: @wrapped_font.dominant_horizontal_stem_width,
225
- StemV: @wrapped_font.dominant_vertical_stem_width || 0})
214
+ fd = document.add({Type: :FontDescriptor,
215
+ FontName: @wrapped_font.font_name.intern,
216
+ FontWeight: @wrapped_font.weight_class,
217
+ FontBBox: @wrapped_font.bounding_box,
218
+ ItalicAngle: @wrapped_font.italic_angle || 0,
219
+ Ascent: @wrapped_font.ascender || 0,
220
+ Descent: @wrapped_font.descender || 0,
221
+ CapHeight: @wrapped_font.cap_height,
222
+ XHeight: @wrapped_font.x_height,
223
+ StemH: @wrapped_font.dominant_horizontal_stem_width,
224
+ StemV: @wrapped_font.dominant_vertical_stem_width || 0})
226
225
  fd.flag(:fixed_pitch) if @wrapped_font.metrics.is_fixed_pitch
227
226
  fd.flag(@wrapped_font.metrics.character_set == 'Special' ? :symbolic : :nonsymbolic)
228
- fd.must_be_indirect = true
229
227
 
230
- dict = document.wrap({Type: :Font, Subtype: :Type1,
231
- BaseFont: @wrapped_font.font_name.intern,
232
- FontDescriptor: fd})
228
+ dict = document.add({Type: :Font, Subtype: :Type1,
229
+ BaseFont: @wrapped_font.font_name.intern,
230
+ FontDescriptor: fd})
233
231
  dict.font_wrapper = self
234
232
 
235
233
  document.register_listener(:complete_objects) do
@@ -51,13 +51,16 @@ module HexaPDF
51
51
  # * If width or height is set to zero, they are determined automatically during layouting.
52
52
  class Box
53
53
 
54
- # Creates a new Box object, using the provided block as drawing block (see ::new). Any
55
- # additional keyword arguments are used for creating the box's Style object.
54
+ # Creates a new Box object, using the provided block as drawing block (see ::new).
56
55
  #
57
56
  # If +content_box+ is +true+, the width and height are taken to mean the content width and
58
57
  # height and the style's padding and border are removed from them appropriately.
59
- def self.create(width: 0, height: 0, content_box: false, **style, &block)
60
- style = Style.new(**style)
58
+ #
59
+ # The +style+ argument defines the Style object (see Style::create for details) for the box.
60
+ # Any additional keyword arguments have to be style properties and are applied to the style
61
+ # object.
62
+ def self.create(width: 0, height: 0, content_box: false, style: nil, **style_properties, &block)
63
+ style = Style.create(style).update(**style_properties)
61
64
  if content_box
62
65
  width += style.padding.left + style.padding.right +
63
66
  style.border.width.left + style.border.width.right
@@ -81,12 +84,12 @@ module HexaPDF
81
84
  # * Style#background_alpha
82
85
  # * Style#padding
83
86
  # * Style#border
84
- # * Style#overlay_callback
85
- # * Style#underlay_callback
87
+ # * Style#overlays
88
+ # * Style#underlays
86
89
  attr_reader :style
87
90
 
88
91
  # :call-seq:
89
- # Box.new(width: 0, height: 0, style: Style.new) {|canv, box| block} -> box
92
+ # Box.new(width: 0, height: 0, style: nil) {|canv, box| block} -> box
90
93
  #
91
94
  # Creates a new Box object with the given width and height that uses the provided block when
92
95
  # it is asked to draw itself on a canvas (see #draw).
@@ -94,10 +97,10 @@ module HexaPDF
94
97
  # Since the final location of the box is not known beforehand, the drawing operations inside
95
98
  # the block should draw inside the rectangle (0, 0, content_width, content_height) - note that
96
99
  # the width and height of the box may not be known beforehand.
97
- def initialize(width: 0, height: 0, style: Style.new, &block)
100
+ def initialize(width: 0, height: 0, style: nil, &block)
98
101
  @width = @initial_width = width
99
102
  @height = @initial_height = height
100
- @style = (style.kind_of?(Style) ? style : Style.new(**style))
103
+ @style = Style.create(style)
101
104
  @draw_block = block
102
105
  end
103
106
 
@@ -0,0 +1,168 @@
1
+ # -*- encoding: utf-8; frozen_string_literal: true -*-
2
+ #
3
+ #--
4
+ # This file is part of HexaPDF.
5
+ #
6
+ # HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby
7
+ # Copyright (C) 2014-2022 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+ require 'hexapdf/layout/box'
37
+
38
+ module HexaPDF
39
+ module Layout
40
+
41
+ # A ColumnBox arranges boxes in one or more columns.
42
+ #
43
+ # The number of columns as well as the size of the gap between the columns can be modified.
44
+ class ColumnBox < Box
45
+
46
+ # The child boxes of this ColumnBox.
47
+ attr_reader :children
48
+
49
+ # The number of columns.
50
+ # TODO: allow array with column widths later like [100, :*, :*]; same for gaps
51
+ attr_reader :columns
52
+
53
+ # The size of the gap between the columns.
54
+ attr_reader :gap
55
+
56
+ # Creates a new ColumnBox object for the given +children+ boxes.
57
+ def initialize(children = [], columns = 2, gap: 36, **kwargs)
58
+ super(**kwargs)
59
+ @children = children
60
+ @columns = columns
61
+ @gap = gap
62
+ end
63
+
64
+ # Fits the column box into the available space.
65
+ def fit(available_width, available_height, frame)
66
+ last_height_difference = 1_000_000
67
+ height = if style.position == :flow
68
+ frame.height
69
+ else
70
+ (@initial_height > 0 ? @initial_height : available_height) - reserved_height
71
+ end
72
+ while true
73
+ p '-'*100
74
+ @frames = []
75
+ if style.position == :flow
76
+ column_width = (frame.width - gap * (@columns - 1)).to_f / @columns
77
+ @columns.times do |col_nr|
78
+ left = (column_width + gap) * col_nr + frame.left
79
+ bottom = frame.bottom
80
+ rect = Geom2D::Polygon([left, bottom],
81
+ [left + column_width, bottom],
82
+ [left + column_width, bottom + height],
83
+ [left, bottom + height])
84
+ shape = Geom2D::Algorithms::PolygonOperation.run(frame.shape, rect, :intersection)
85
+ col_frame = Frame.new(left, bottom, column_width, height)
86
+ col_frame.shape = shape
87
+ @frames << col_frame
88
+ end
89
+ @frame_index = 0
90
+ @results = @children.map {|child_box| fit_box(child_box) }
91
+ @width = frame.width
92
+ @height = frame.height - @frames.min_by(&:y).y
93
+ else
94
+ width = (@initial_width > 0 ? @initial_width : available_width) - reserved_width
95
+ column_width = (width - gap * (@columns - 1)).to_f / @columns
96
+ @columns.times do |col_nr|
97
+ @frames << Frame.new((column_width + gap) * col_nr, 0, column_width, height)
98
+ end
99
+ @frame_index = 0
100
+ @results = @children.map {|child_box| fit_box(child_box) }
101
+ @width = width
102
+ @height = height - @frames.min_by(&:y).y
103
+ end
104
+ min_y, max_y = @frames.minmax_by(&:y).map(&:y)
105
+ p [height, @frames.map(&:y), last_height_difference, min_y, max_y]
106
+ # TOOD: @result.any?(&:empty?) only for the first run!!!! if the first run fails, we
107
+ # cannot balance the columns because there is too much content.
108
+ # TODO: another break condition is if the @results didn't change since the last run
109
+ p [:maybe_redo, min_y, max_y, height, last_height_difference]
110
+ p [@results.map {|arr| arr.all? {|r| r.status }}]
111
+ break if max_y != height && @results.all? {|arr| !arr.empty? && arr.all? {|r| r.success? }} &&
112
+ (@results.any?(&:empty?) ||
113
+ max_y - min_y >= last_height_difference ||
114
+ max_y - min_y < 0.5)
115
+ if max_y == 0 && min_y == 0
116
+ height += last_height_difference / 4.0
117
+ else
118
+ last_height_difference = max_y - min_y
119
+ height -= last_height_difference / 2.0
120
+ end
121
+ end
122
+ @results.all? {|res| res.length == 1 }
123
+ end
124
+
125
+ private
126
+
127
+ def fit_box(box)
128
+ cur_frame = @frames[@frame_index]
129
+ fit_results = []
130
+ while cur_frame
131
+ result = cur_frame.fit(box)
132
+ if result.success?
133
+ cur_frame.remove_area(result.mask)
134
+ fit_results << result
135
+ break
136
+ elsif cur_frame.full?
137
+ @frame_index += 1
138
+ break if @frame_index == @frames.length
139
+ cur_frame = @frames[@frame_index]
140
+ else
141
+ draw_box, box = cur_frame.split(result)
142
+ if draw_box
143
+ cur_frame.remove_area(result.mask)
144
+ fit_results << result
145
+ elsif !cur_frame.find_next_region
146
+ @frame_index += 1
147
+ break if @frame_index == @frames.length
148
+ cur_frame = @frames[@frame_index]
149
+ end
150
+ end
151
+ end
152
+ fit_results
153
+ end
154
+
155
+ # Draws the child boxes onto the canvas at position [x, y].
156
+ def draw_content(canvas, x, y)
157
+ x = y = 0 if style.position == :flow
158
+ @results.each do |result_boxes|
159
+ result_boxes.each do |result|
160
+ result.box.draw(canvas, x + result.x, y + result.y)
161
+ end
162
+ end
163
+ end
164
+
165
+ end
166
+
167
+ end
168
+ end