hexapdf 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (122) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +76 -2
  3. data/CONTRIBUTERS +1 -1
  4. data/Rakefile +1 -1
  5. data/VERSION +1 -1
  6. data/examples/boxes.rb +68 -0
  7. data/examples/graphics.rb +12 -12
  8. data/examples/{text_box_alignment.rb → text_layouter_alignment.rb} +14 -14
  9. data/examples/text_layouter_inline_boxes.rb +66 -0
  10. data/examples/{text_box_line_wrapping.rb → text_layouter_line_wrapping.rb} +9 -10
  11. data/examples/{text_box_shapes.rb → text_layouter_shapes.rb} +58 -54
  12. data/examples/text_layouter_styling.rb +125 -0
  13. data/examples/truetype.rb +5 -7
  14. data/lib/hexapdf/cli/command.rb +1 -0
  15. data/lib/hexapdf/configuration.rb +170 -106
  16. data/lib/hexapdf/content/canvas.rb +41 -36
  17. data/lib/hexapdf/content/graphics_state.rb +15 -0
  18. data/lib/hexapdf/content/operator.rb +1 -1
  19. data/lib/hexapdf/dictionary.rb +20 -8
  20. data/lib/hexapdf/dictionary_fields.rb +8 -6
  21. data/lib/hexapdf/document.rb +25 -26
  22. data/lib/hexapdf/document/fonts.rb +4 -4
  23. data/lib/hexapdf/document/images.rb +2 -2
  24. data/lib/hexapdf/document/pages.rb +16 -16
  25. data/lib/hexapdf/encryption/security_handler.rb +41 -9
  26. data/lib/hexapdf/filter/flate_decode.rb +1 -1
  27. data/lib/hexapdf/filter/lzw_decode.rb +1 -1
  28. data/lib/hexapdf/filter/predictor.rb +7 -1
  29. data/lib/hexapdf/font/true_type/font.rb +20 -0
  30. data/lib/hexapdf/font/type1/font.rb +23 -0
  31. data/lib/hexapdf/font_loader.rb +1 -0
  32. data/lib/hexapdf/font_loader/from_configuration.rb +2 -3
  33. data/lib/hexapdf/font_loader/from_file.rb +65 -0
  34. data/lib/hexapdf/image_loader/png.rb +2 -2
  35. data/lib/hexapdf/layout.rb +3 -2
  36. data/lib/hexapdf/layout/box.rb +146 -0
  37. data/lib/hexapdf/layout/inline_box.rb +40 -31
  38. data/lib/hexapdf/layout/{line_fragment.rb → line.rb} +12 -13
  39. data/lib/hexapdf/layout/style.rb +630 -41
  40. data/lib/hexapdf/layout/text_fragment.rb +80 -12
  41. data/lib/hexapdf/layout/{text_box.rb → text_layouter.rb} +164 -109
  42. data/lib/hexapdf/number_tree_node.rb +1 -1
  43. data/lib/hexapdf/parser.rb +4 -1
  44. data/lib/hexapdf/revisions.rb +11 -4
  45. data/lib/hexapdf/stream.rb +8 -9
  46. data/lib/hexapdf/tokenizer.rb +5 -3
  47. data/lib/hexapdf/type.rb +3 -0
  48. data/lib/hexapdf/type/action.rb +56 -0
  49. data/lib/hexapdf/type/actions.rb +52 -0
  50. data/lib/hexapdf/type/actions/go_to.rb +52 -0
  51. data/lib/hexapdf/type/actions/go_to_r.rb +54 -0
  52. data/lib/hexapdf/type/actions/launch.rb +73 -0
  53. data/lib/hexapdf/type/actions/uri.rb +65 -0
  54. data/lib/hexapdf/type/annotation.rb +85 -0
  55. data/lib/hexapdf/type/annotations.rb +51 -0
  56. data/lib/hexapdf/type/annotations/link.rb +70 -0
  57. data/lib/hexapdf/type/annotations/markup_annotation.rb +70 -0
  58. data/lib/hexapdf/type/annotations/text.rb +81 -0
  59. data/lib/hexapdf/type/catalog.rb +3 -1
  60. data/lib/hexapdf/type/embedded_file.rb +6 -11
  61. data/lib/hexapdf/type/file_specification.rb +4 -6
  62. data/lib/hexapdf/type/font.rb +3 -1
  63. data/lib/hexapdf/type/font_descriptor.rb +18 -16
  64. data/lib/hexapdf/type/form.rb +3 -1
  65. data/lib/hexapdf/type/graphics_state_parameter.rb +3 -1
  66. data/lib/hexapdf/type/image.rb +4 -2
  67. data/lib/hexapdf/type/info.rb +2 -5
  68. data/lib/hexapdf/type/names.rb +2 -5
  69. data/lib/hexapdf/type/object_stream.rb +2 -1
  70. data/lib/hexapdf/type/page.rb +14 -1
  71. data/lib/hexapdf/type/page_tree_node.rb +9 -6
  72. data/lib/hexapdf/type/resources.rb +2 -5
  73. data/lib/hexapdf/type/trailer.rb +2 -5
  74. data/lib/hexapdf/type/viewer_preferences.rb +2 -5
  75. data/lib/hexapdf/type/xref_stream.rb +3 -1
  76. data/lib/hexapdf/version.rb +1 -1
  77. data/test/hexapdf/common_tokenizer_tests.rb +3 -1
  78. data/test/hexapdf/content/test_canvas.rb +29 -3
  79. data/test/hexapdf/content/test_graphics_state.rb +11 -0
  80. data/test/hexapdf/content/test_operator.rb +3 -2
  81. data/test/hexapdf/document/test_fonts.rb +8 -8
  82. data/test/hexapdf/document/test_images.rb +4 -12
  83. data/test/hexapdf/document/test_pages.rb +7 -7
  84. data/test/hexapdf/encryption/test_security_handler.rb +1 -5
  85. data/test/hexapdf/filter/test_predictor.rb +40 -12
  86. data/test/hexapdf/font/true_type/test_font.rb +16 -0
  87. data/test/hexapdf/font/type1/test_font.rb +30 -0
  88. data/test/hexapdf/font_loader/test_from_file.rb +29 -0
  89. data/test/hexapdf/font_loader/test_standard14.rb +4 -3
  90. data/test/hexapdf/layout/test_box.rb +104 -0
  91. data/test/hexapdf/layout/test_inline_box.rb +24 -10
  92. data/test/hexapdf/layout/{test_line_fragment.rb → test_line.rb} +9 -9
  93. data/test/hexapdf/layout/test_style.rb +519 -31
  94. data/test/hexapdf/layout/test_text_fragment.rb +136 -15
  95. data/test/hexapdf/layout/{test_text_box.rb → test_text_layouter.rb} +224 -144
  96. data/test/hexapdf/layout/test_text_shaper.rb +1 -1
  97. data/test/hexapdf/test_configuration.rb +12 -6
  98. data/test/hexapdf/test_dictionary.rb +27 -2
  99. data/test/hexapdf/test_dictionary_fields.rb +10 -1
  100. data/test/hexapdf/test_document.rb +14 -13
  101. data/test/hexapdf/test_parser.rb +12 -0
  102. data/test/hexapdf/test_revisions.rb +34 -0
  103. data/test/hexapdf/test_stream.rb +1 -1
  104. data/test/hexapdf/test_type.rb +18 -0
  105. data/test/hexapdf/test_writer.rb +2 -2
  106. data/test/hexapdf/type/actions/test_launch.rb +24 -0
  107. data/test/hexapdf/type/actions/test_uri.rb +23 -0
  108. data/test/hexapdf/type/annotations/test_link.rb +19 -0
  109. data/test/hexapdf/type/annotations/test_markup_annotation.rb +22 -0
  110. data/test/hexapdf/type/annotations/test_text.rb +38 -0
  111. data/test/hexapdf/type/test_annotation.rb +38 -0
  112. data/test/hexapdf/type/test_file_specification.rb +0 -7
  113. data/test/hexapdf/type/test_info.rb +0 -5
  114. data/test/hexapdf/type/test_page.rb +14 -0
  115. data/test/hexapdf/type/test_page_tree_node.rb +4 -1
  116. data/test/hexapdf/type/test_trailer.rb +0 -4
  117. data/test/test_helper.rb +6 -3
  118. metadata +36 -15
  119. data/examples/text_box_inline_boxes.rb +0 -56
  120. data/examples/text_box_styling.rb +0 -72
  121. data/test/hexapdf/type/test_embedded_file.rb +0 -16
  122. data/test/hexapdf/type/test_names.rb +0 -9
@@ -132,6 +132,32 @@ module HexaPDF
132
132
  # EncryptionDictionary class.
133
133
  class SecurityHandler
134
134
 
135
+ # Provides additional encryption specific information for HexaPDF::StreamData objects.
136
+ class EncryptedStreamData < StreamData
137
+
138
+ # The encryption key.
139
+ attr_reader :key
140
+
141
+ # The encryption algorithm.
142
+ attr_reader :algorithm
143
+
144
+ # Creates a new encrypted stream data object by utilizing the given stream data object as
145
+ # template. The arguments +key+ and +algorithm+ are used for decrypting purposes.
146
+ def initialize(obj, key, algorithm)
147
+ obj.instance_variables.each {|v| instance_variable_set(v, obj.instance_variable_get(v))}
148
+ @key = key
149
+ @algorithm = algorithm
150
+ end
151
+
152
+ alias :undecrypted_fiber :fiber
153
+
154
+ # Returns a fiber like HexaPDF::StreamData#fiber, but one wrapped in a decrypting fiber.
155
+ def fiber(*args)
156
+ @algorithm.decryption_fiber(@key, super(*args))
157
+ end
158
+
159
+ end
160
+
135
161
  # :call-seq:
136
162
  # SecurityHandler.set_up_encryption(document, handler_name, **options) -> handler
137
163
  #
@@ -143,8 +169,8 @@ module HexaPDF
143
169
  #
144
170
  # See: #set_up_encryption (for the common encryption options).
145
171
  def self.set_up_encryption(document, handler_name, **options)
146
- handler = GlobalConfiguration.constantize('encryption.filter_map', handler_name) do
147
- GlobalConfiguration.constantize('encryption.sub_filter_map', handler_name) do
172
+ handler = document.config.constantize('encryption.filter_map', handler_name) do
173
+ document.config.constantize('encryption.sub_filter_map', handler_name) do
148
174
  raise HexaPDF::EncryptionError, "Could not find the specified security handler"
149
175
  end
150
176
  end
@@ -170,8 +196,8 @@ module HexaPDF
170
196
  if dict.nil?
171
197
  raise HexaPDF::EncryptionError, "No /Encrypt dictionary found"
172
198
  end
173
- handler = HexaPDF::GlobalConfiguration.constantize('encryption.filter_map', dict[:Filter]) do
174
- HexaPDF::GlobalConfiguration.constantize('encryption.sub_filter_map', dict[:SubFilter]) do
199
+ handler = document.config.constantize('encryption.filter_map', dict[:Filter]) do
200
+ document.config.constantize('encryption.sub_filter_map', dict[:SubFilter]) do
175
201
  raise HexaPDF::EncryptionError, "Could not find a suitable security handler"
176
202
  end
177
203
  end
@@ -237,8 +263,7 @@ module HexaPDF
237
263
  unless string_algorithm == stream_algorithm
238
264
  key = object_key(obj.oid, obj.gen, stream_algorithm)
239
265
  end
240
- obj.raw_stream.filter.unshift(:Encryption)
241
- obj.raw_stream.decode_parms.unshift(key: key, algorithm: stream_algorithm)
266
+ obj.data.stream = EncryptedStreamData.new(obj.raw_stream, key, stream_algorithm)
242
267
  end
243
268
 
244
269
  obj
@@ -259,7 +284,14 @@ module HexaPDF
259
284
  return obj.stream_encoder if obj.type == :XRef
260
285
 
261
286
  key = object_key(obj.oid, obj.gen, stream_algorithm)
262
- obj.stream_encoder(:Encryption, key: key, algorithm: stream_algorithm)
287
+ source = obj.stream_source
288
+ result = obj.stream_encoder(source)
289
+ if result == source && obj.raw_stream.kind_of?(EncryptedStreamData) &&
290
+ obj.raw_stream.key == key && obj.raw_stream.algorithm == stream_algorithm
291
+ obj.raw_stream.undecrypted_fiber
292
+ else
293
+ stream_algorithm.encryption_fiber(key, result)
294
+ end
263
295
  end
264
296
 
265
297
  # Computes the encryption key and sets up the algorithms for encrypting the document based on
@@ -420,12 +452,12 @@ module HexaPDF
420
452
 
421
453
  # Returns the class that is used for ARC4 encryption.
422
454
  def arc4_algorithm
423
- @arc4_algorithm ||= HexaPDF::GlobalConfiguration.constantize('encryption.arc4')
455
+ @arc4_algorithm ||= document.config.constantize('encryption.arc4')
424
456
  end
425
457
 
426
458
  # Returns the class that is used for AES encryption.
427
459
  def aes_algorithm
428
- @aes_algorithm ||= HexaPDF::GlobalConfiguration.constantize('encryption.aes')
460
+ @aes_algorithm ||= document.config.constantize('encryption.aes')
429
461
  end
430
462
 
431
463
  # Returns the class that is used for the identity algorithm which passes back the data as is
@@ -81,7 +81,7 @@ module HexaPDF
81
81
  Fiber.yield(data)
82
82
  end
83
83
  begin
84
- inflater.finish
84
+ (data = inflater.finish).empty? ? nil : data
85
85
  rescue
86
86
  raise FilterError, "Problem while decoding Flate encoded stream: #{$!}"
87
87
  end
@@ -85,7 +85,7 @@ module HexaPDF
85
85
  case table.size
86
86
  when 510, 1022, 2046
87
87
  code_length += 1
88
- when 4095
88
+ when 4096
89
89
  if code != CLEAR_TABLE
90
90
  raise FilterError, "Maximum of 12bit for codes in LZW stream exceeded"
91
91
  end
@@ -33,6 +33,7 @@
33
33
 
34
34
  require 'fiber'
35
35
  require 'hexapdf/error'
36
+ require 'hexapdf/configuration'
36
37
  require 'hexapdf/utils/bit_stream'
37
38
 
38
39
  module HexaPDF
@@ -254,8 +255,13 @@ module HexaPDF
254
255
  Fiber.yield(result) unless result.empty?
255
256
  end
256
257
 
257
- unless pos == data.length
258
+ if pos != data.length && GlobalConfiguration['filter.predictor.strict']
258
259
  raise FilterError, "Data is missing for PNG predictor"
260
+ elsif pos != data.length && data.length != 1
261
+ result = ''.b
262
+ bytes_per_row = data.length - pos
263
+ row_action.call(result)
264
+ result
259
265
  end
260
266
  end
261
267
  end
@@ -162,6 +162,26 @@ module HexaPDF
162
162
  weight / 5
163
163
  end
164
164
 
165
+ # Returns the distance from the baseline to the top of the underline.
166
+ def underline_position
167
+ self[:post].underline_position
168
+ end
169
+
170
+ # Returns the stroke width for the underline.
171
+ def underline_thickness
172
+ self[:post].underline_thickness
173
+ end
174
+
175
+ # Returns the distance from the baseline to the top of the strikeout line.
176
+ def strikeout_position
177
+ self[:"OS/2"].strikeout_position
178
+ end
179
+
180
+ # Returns the stroke width for the strikeout line.
181
+ def strikeout_thickness
182
+ self[:"OS/2"].strikeout_size
183
+ end
184
+
165
185
  # Returns th glyph ID of the missing glyph, i.e. 0.
166
186
  def missing_glyph_id
167
187
  0
@@ -76,6 +76,7 @@ module HexaPDF
76
76
  def_delegators :@metrics, :weight, :weight_class, :bounding_box, :italic_angle
77
77
  def_delegators :@metrics, :ascender, :descender, :cap_height, :x_height
78
78
  def_delegators :@metrics, :dominant_horizontal_stem_width, :dominant_vertical_stem_width
79
+ def_delegators :@metrics, :underline_thickness
79
80
 
80
81
  # Creates a new Type1 font object with the given font metrics.
81
82
  def initialize(metrics)
@@ -128,6 +129,28 @@ module HexaPDF
128
129
  end
129
130
  end
130
131
 
132
+ # Returns the distance from the baseline to the top of the underline.
133
+ def underline_position
134
+ @metrics.underline_position + @metrics.underline_thickness / 2.0
135
+ end
136
+
137
+ # Returns the distance from the baseline to the top of the strikeout line.
138
+ def strikeout_position
139
+ # We use the suggested value from the OpenType spec.
140
+ 225
141
+ end
142
+
143
+ # Returns the thickness of the strikeout line.
144
+ def strikeout_thickness
145
+ # The OpenType spec suggests the width of an em-dash or 50, so we use that as
146
+ # approximation since the AFM files don't contain this information.
147
+ if (bbox = @metrics.character_metrics[:emdash]&.bbox)
148
+ bbox[3] - bbox[1]
149
+ else
150
+ 50
151
+ end
152
+ end
153
+
131
154
  end
132
155
 
133
156
  end
@@ -79,6 +79,7 @@ module HexaPDF
79
79
 
80
80
  autoload(:Standard14, 'hexapdf/font_loader/standard14')
81
81
  autoload(:FromConfiguration, 'hexapdf/font_loader/from_configuration')
82
+ autoload(:FromFile, 'hexapdf/font_loader/from_file')
82
83
 
83
84
  end
84
85
 
@@ -32,6 +32,7 @@
32
32
  #++
33
33
 
34
34
  require 'hexapdf/font/true_type_wrapper'
35
+ require 'hexapdf/font_loader/from_file'
35
36
 
36
37
  module HexaPDF
37
38
  module FontLoader
@@ -62,9 +63,7 @@ module HexaPDF
62
63
  unless File.file?(file)
63
64
  raise HexaPDF::Error, "The configured font file #{file} does not exist"
64
65
  end
65
-
66
- font = HexaPDF::Font::TrueType::Font.new(File.open(file))
67
- HexaPDF::Font::TrueTypeWrapper.new(document, font, subset: subset)
66
+ FromFile.call(document, file, subset: subset)
68
67
  end
69
68
 
70
69
  end
@@ -0,0 +1,65 @@
1
+ # -*- encoding: utf-8 -*-
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-2017 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
+
34
+ require 'hexapdf/font/true_type_wrapper'
35
+
36
+ module HexaPDF
37
+ module FontLoader
38
+
39
+ # This module interprets the font name as file name and tries to load it.
40
+ module FromFile
41
+
42
+ # Loads the given font by interpreting the font name as file name.
43
+ #
44
+ # The file object representing the font file is *not* closed and if needed must be closed by
45
+ # the caller once the font is not needed anymore.
46
+ #
47
+ # +document+::
48
+ # The PDF document to associate the font object with.
49
+ #
50
+ # +name+::
51
+ # The file name.
52
+ #
53
+ # +subset+::
54
+ # Specifies whether the font should be subset if possible.
55
+ def self.call(document, name, subset: true, **)
56
+ return nil unless File.file?(name)
57
+
58
+ font = HexaPDF::Font::TrueType::Font.new(File.open(name, 'rb'))
59
+ HexaPDF::Font::TrueTypeWrapper.new(document, font, subset: subset)
60
+ end
61
+
62
+ end
63
+
64
+ end
65
+ end
@@ -347,7 +347,7 @@ module HexaPDF
347
347
  image_data = ''.b
348
348
  mask_data = ''.b
349
349
 
350
- flate_decode = GlobalConfiguration.constantize('filter.map', :FlateDecode)
350
+ flate_decode = @document.config.constantize('filter.map', :FlateDecode)
351
351
  source = flate_decode.decoder(Fiber.new(&image_data_proc(offset)))
352
352
 
353
353
  data = ''.b
@@ -381,7 +381,7 @@ module HexaPDF
381
381
  bpc = decode_parms[:BitsPerComponent]
382
382
  bytes_per_row = (width * bpc + 7) / 8 + 1
383
383
 
384
- flate_decode = GlobalConfiguration.constantize('filter.map', :FlateDecode)
384
+ flate_decode = @document.config.constantize('filter.map', :FlateDecode)
385
385
  source = flate_decode.decoder(Fiber.new(&image_data_proc(offset)))
386
386
 
387
387
  mask_data = ''.b
@@ -42,9 +42,10 @@ module HexaPDF
42
42
  autoload(:Style, 'hexapdf/layout/style')
43
43
  autoload(:TextFragment, 'hexapdf/layout/text_fragment')
44
44
  autoload(:InlineBox, 'hexapdf/layout/inline_box')
45
- autoload(:LineFragment, 'hexapdf/layout/line_fragment')
45
+ autoload(:Line, 'hexapdf/layout/line')
46
46
  autoload(:TextShaper, 'hexapdf/layout/text_shaper')
47
- autoload(:TextBox, 'hexapdf/layout/text_box')
47
+ autoload(:TextLayouter, 'hexapdf/layout/text_layouter')
48
+ autoload(:Box, 'hexapdf/layout/box')
48
49
 
49
50
  end
50
51
 
@@ -0,0 +1,146 @@
1
+ # -*- encoding: utf-8 -*-
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-2017 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
+ require 'hexapdf/layout/style'
34
+
35
+ module HexaPDF
36
+ module Layout
37
+
38
+ # The base class for all layout boxes.
39
+ #
40
+ # HexaPDF uses the following box model:
41
+ #
42
+ # * Each box can specify a content width and content height. Padding, border and margin are
43
+ # *outside* of this content rectangle.
44
+ #
45
+ # * The #width and #height accessors can be used to get the width and height of the box
46
+ # including padding and the border.
47
+ #
48
+ class Box
49
+
50
+ # The width of the content box, i.e. without padding or borders.
51
+ #
52
+ # The value 0 means that the width is dynamically determined.
53
+ attr_reader :content_width
54
+
55
+ # The height of the content box, i.e. without padding or borders.
56
+ #
57
+ # The value 0 means that the height is dynamically determined.
58
+ attr_reader :content_height
59
+
60
+ # The style to be applied.
61
+ #
62
+ # Only the following properties are used:
63
+ #
64
+ # * Style#background_color
65
+ # * Style#padding
66
+ # * Style#border
67
+ # * Style#overlay_callback
68
+ # * Style#underlay_callback
69
+ attr_reader :style
70
+
71
+ # :call-seq:
72
+ # Box.new(content_width: 0, content_height: 0, style: Style.new) {|canvas, box| block} -> box
73
+ # Box.new(width: 0, height: 0, style: Style.new) {|canvas, box| block} -> box
74
+ #
75
+ # Creates a new Box object with the given width and height for its content that uses the
76
+ # provided block when it is asked to draw itself on a canvas (see #draw).
77
+ #
78
+ # Alternative to specifying the content width/height, it is also possible to specify the box
79
+ # width/height. The content width is then immediately calculated using the border and padding
80
+ # information from the style and stored.
81
+ #
82
+ # Since the final location of the box is not known beforehand, the drawing operations inside
83
+ # the block should draw inside the rectangle (0, 0, content_width, content_height) - note that
84
+ # the width and height of the box may not be known beforehand.
85
+ def initialize(content_width: 0, content_height: 0, width: 0, height: 0,
86
+ style: Style.new, &block)
87
+ @style = (style.kind_of?(Style) ? style : Style.new(style))
88
+ @draw_block = block
89
+ @content_width = content_width
90
+ @content_width = [width - self.width, 0].max if width != 0 && @content_width == 0
91
+ @content_height = content_height
92
+ @content_height = [height - self.height, 0].max if height != 0 && @content_height == 0
93
+ end
94
+
95
+ # Returns the width of the box, including padding and border widths.
96
+ def width
97
+ @content_width + @style.padding.left + @style.padding.right +
98
+ @style.border.width.left + @style.border.width.right
99
+ end
100
+
101
+ # Returns the height of the box, including padding and border widths.
102
+ def height
103
+ @content_height + @style.padding.top + @style.padding.bottom +
104
+ @style.border.width.top + @style.border.width.bottom
105
+ end
106
+
107
+ # :call-seq:
108
+ # box.draw(canvas, x, y)
109
+ #
110
+ # Draws the contents of the box onto the canvas at the position (x, y).
111
+ #
112
+ # The coordinate system is translated so that the origin is at the lower left corner of the
113
+ # contents box during the drawing operations.
114
+ def draw(canvas, x, y)
115
+ if style.background_color
116
+ canvas.save_graphics_state do
117
+ canvas.fill_color(style.background_color).rectangle(x, y, width, height).fill
118
+ end
119
+ end
120
+
121
+ style.underlays.draw(canvas, x, y, self)
122
+
123
+ unless style.border.none?
124
+ style.border.draw(canvas, x, y, width, height)
125
+ end
126
+
127
+ if @draw_block
128
+ canvas.translate(x + style.padding.left + style.border.width.left,
129
+ y + style.padding.bottom + style.border.width.bottom) do
130
+ @draw_block.call(canvas, self)
131
+ end
132
+ end
133
+
134
+ style.overlays.draw(canvas, x, y, self)
135
+ end
136
+
137
+ # Returns +true+ if no drawing operations are performed.
138
+ def empty?
139
+ !(@draw_block || style.background_color || !style.underlays.none? ||
140
+ !style.border.none? || !style.overlays.none?)
141
+ end
142
+
143
+ end
144
+
145
+ end
146
+ end