hexapdf 1.1.1 → 1.3.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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +79 -0
  3. data/README.md +1 -1
  4. data/lib/hexapdf/cli/command.rb +63 -63
  5. data/lib/hexapdf/cli/inspect.rb +14 -5
  6. data/lib/hexapdf/cli/modify.rb +0 -1
  7. data/lib/hexapdf/cli/optimize.rb +5 -5
  8. data/lib/hexapdf/composer.rb +14 -0
  9. data/lib/hexapdf/configuration.rb +26 -0
  10. data/lib/hexapdf/content/graphics_state.rb +1 -1
  11. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
  12. data/lib/hexapdf/document/annotations.rb +173 -0
  13. data/lib/hexapdf/document/layout.rb +45 -6
  14. data/lib/hexapdf/document.rb +28 -7
  15. data/lib/hexapdf/error.rb +11 -3
  16. data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
  17. data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
  18. data/lib/hexapdf/font/type1_wrapper.rb +1 -0
  19. data/lib/hexapdf/layout/style.rb +101 -7
  20. data/lib/hexapdf/object.rb +2 -2
  21. data/lib/hexapdf/pdf_array.rb +25 -3
  22. data/lib/hexapdf/tokenizer.rb +4 -1
  23. data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
  24. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  25. data/lib/hexapdf/type/acro_form/form.rb +7 -6
  26. data/lib/hexapdf/type/acro_form/java_script_actions.rb +9 -2
  27. data/lib/hexapdf/type/acro_form/text_field.rb +9 -2
  28. data/lib/hexapdf/type/annotation.rb +71 -1
  29. data/lib/hexapdf/type/annotations/appearance_generator.rb +348 -0
  30. data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
  31. data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
  32. data/lib/hexapdf/type/annotations/circle.rb +65 -0
  33. data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
  34. data/lib/hexapdf/type/annotations/line.rb +490 -0
  35. data/lib/hexapdf/type/annotations/square.rb +65 -0
  36. data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
  37. data/lib/hexapdf/type/annotations/widget.rb +52 -116
  38. data/lib/hexapdf/type/annotations.rb +8 -0
  39. data/lib/hexapdf/type/form.rb +2 -2
  40. data/lib/hexapdf/version.rb +1 -1
  41. data/lib/hexapdf/writer.rb +0 -1
  42. data/lib/hexapdf/xref_section.rb +7 -4
  43. data/test/hexapdf/content/test_graphics_state.rb +2 -3
  44. data/test/hexapdf/content/test_operator.rb +4 -5
  45. data/test/hexapdf/digital_signature/test_cms_handler.rb +7 -8
  46. data/test/hexapdf/digital_signature/test_handler.rb +2 -3
  47. data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +1 -2
  48. data/test/hexapdf/document/test_annotations.rb +55 -0
  49. data/test/hexapdf/document/test_layout.rb +24 -2
  50. data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
  51. data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
  52. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  53. data/test/hexapdf/layout/test_style.rb +27 -2
  54. data/test/hexapdf/task/test_optimize.rb +1 -1
  55. data/test/hexapdf/test_composer.rb +7 -0
  56. data/test/hexapdf/test_document.rb +11 -3
  57. data/test/hexapdf/test_object.rb +1 -1
  58. data/test/hexapdf/test_pdf_array.rb +36 -3
  59. data/test/hexapdf/test_stream.rb +1 -2
  60. data/test/hexapdf/test_xref_section.rb +1 -1
  61. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
  62. data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
  63. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  64. data/test/hexapdf/type/acro_form/test_form.rb +17 -1
  65. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +21 -0
  66. data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
  67. data/test/hexapdf/type/annotations/test_appearance_generator.rb +482 -0
  68. data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
  69. data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
  70. data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
  71. data/test/hexapdf/type/annotations/test_line.rb +169 -0
  72. data/test/hexapdf/type/annotations/test_widget.rb +35 -81
  73. data/test/hexapdf/type/test_annotation.rb +55 -0
  74. data/test/hexapdf/type/test_form.rb +6 -0
  75. metadata +17 -2
@@ -48,6 +48,8 @@ module HexaPDF
48
48
  # See: PDF2.0 s12.5.6.19, HexaPDF::Type::Annotation
49
49
  class Widget < Annotation
50
50
 
51
+ include BorderStyling
52
+
51
53
  # The dictionary used by the /MK key of the widget annotation.
52
54
  class AppearanceCharacteristics < Dictionary
53
55
 
@@ -122,111 +124,21 @@ module HexaPDF
122
124
  end
123
125
  end
124
126
 
125
- # Describes the border of an annotation.
126
- #
127
- # The +color+ property is either +nil+ if the border is transparent or else a device color
128
- # object - see HexaPDF::Content::ColorSpace.
129
- #
130
- # The +style+ property can be one of the following:
131
- #
132
- # :solid:: Solid line.
133
- # :beveled:: Embossed rectangle seemingly raised above the surface of the page.
134
- # :inset:: Engraved rectangle receeding into the page.
135
- # :underlined:: Underlined, i.e. only the bottom border is draw.
136
- # Array: Dash array describing how to dash the line.
137
- BorderStyle = Struct.new(:width, :color, :style, :horizontal_corner_radius,
138
- :vertical_corner_radius)
139
-
140
- # :call-seq:
141
- # widget.border_style => border_style
142
- # widget.border_style(color: 0, width: 1, style: :solid) => widget
143
- #
144
- # Returns a BorderStyle instance representing the border style of the widget when no
145
- # argument is given. Otherwise sets the border style of the widget and returns self.
146
- #
147
- # When setting a border style, arguments that are not provided will use the default: a
148
- # border with a solid, black, 1pt wide line. This also means that multiple invocations will
149
- # reset *all* prior values.
150
- #
151
- # +color+:: The color of the border. See
152
- # HexaPDF::Content::ColorSpace.device_color_from_specification for information on
153
- # the allowed arguments.
154
- #
155
- # If the special value +:transparent+ is used when setting the color, a
156
- # transparent is used. A transparent border will return a +nil+ value when getting
157
- # the border color.
158
- #
159
- # +width+:: The width of the border. If set to 0, no border is shown.
160
- #
161
- # +style+:: Defines how the border is drawn. can be one of the following:
162
- #
163
- # +:solid+:: Draws a solid border.
164
- # +:beveled+:: Draws a beveled border.
165
- # +:inset+:: Draws an inset border.
166
- # +:underlined+:: Draws only the bottom border.
167
- # Array:: An array specifying a line dash pattern (see
168
- # HexaPDF::Content::LineDashPattern)
169
- def border_style(color: nil, width: nil, style: nil)
170
- if color || width || style
171
- color = if color == :transparent
172
- []
173
- else
174
- Content::ColorSpace.device_color_from_specification(color || 0).components
175
- end
176
- width ||= 1
177
- style ||= :solid
178
-
179
- (self[:MK] ||= {})[:BC] = color
180
- bs = self[:BS] = {W: width}
181
- case style
182
- when :solid then bs[:S] = :S
183
- when :beveled then bs[:S] = :B
184
- when :inset then bs[:S] = :I
185
- when :underlined then bs[:S] = :U
186
- when Array
187
- bs[:S] = :D
188
- bs[:D] = style
189
- else
190
- raise ArgumentError, "Unknown value #{style} for style argument"
191
- end
192
- self
193
- else
194
- result = BorderStyle.new(1, nil, :solid, 0, 0)
195
- if (ac = self[:MK]) && (bc = ac[:BC]) && !bc.empty?
196
- result.color = Content::ColorSpace.prenormalized_device_color(bc.value)
197
- end
198
-
199
- if (bs = self[:BS])
200
- result.width = bs[:W] if bs.key?(:W)
201
- result.style = case bs[:S]
202
- when :S then :solid
203
- when :B then :beveled
204
- when :I then :inset
205
- when :U then :underlined
206
- when :D then bs[:D].value
207
- else :solid
208
- end
209
- elsif key?(:Border)
210
- border = self[:Border]
211
- result.horizontal_corner_radius = border[0]
212
- result.vertical_corner_radius = border[1]
213
- result.width = border[2]
214
- result.style = border[3] if border[3]
215
- end
216
-
217
- result
218
- end
219
- end
220
-
221
- # Describes the marker style of a check box or radio button widget.
127
+ # Describes the marker style of a check box, radio button or push button widget.
222
128
  class MarkerStyle
223
129
 
224
- # The kind of marker that is shown inside the widget. Can either be one of the symbols
225
- # +:check+, +:circle+, +:cross+, +:diamond+, +:square+ or +:star+, or a one character
226
- # string. The latter is interpreted using the ZapfDingbats font.
130
+ # The kind of marker that is shown inside the widget.
227
131
  #
228
- # If an empty string is set, it is treated as if +nil+ was set, i.e. it shows the default
229
- # marker for the field type.
132
+ # Radion buttons and check boxes::
133
+ # Can either be one of the symbols +:check+, +:circle+, +:cross+, +:diamond+,
134
+ # +:square+ or +:star+, or a one character string. The latter is interpreted using the
135
+ # ZapfDingbats font.
136
+ #
137
+ # If an empty string is set, it is treated as if +nil+ was set, i.e. it shows the
138
+ # default marker for the field type.
139
+ #
140
+ # Push buttons:
141
+ # The caption string.
230
142
  attr_reader :style
231
143
 
232
144
  # The size of the marker in PDF points that is shown inside the widget. The special value
@@ -237,27 +149,40 @@ module HexaPDF
237
149
  # HexaPDF::Content::ColorSpace.
238
150
  attr_reader :color
239
151
 
152
+ # The resource name of the font that should be used for the caption.
153
+ #
154
+ # This is only used for push button widgets.
155
+ attr_reader :font_name
156
+
240
157
  # Creates a new instance with the given values.
241
- def initialize(style, size, color)
158
+ def initialize(style, size, color, font_name)
242
159
  @style = style
243
160
  @size = size
244
161
  @color = color
162
+ @font_name = font_name
245
163
  end
246
164
 
247
165
  end
248
166
 
249
167
  # :call-seq:
250
- # widget.marker_style => marker_style
251
- # widget.marker_style(style: nil, size: nil, color: nil) => widget
168
+ # widget.marker_style => marker_style
169
+ # widget.marker_style(style: nil, size: nil, color: nil, font_name: nil) => widget
252
170
  #
253
171
  # Returns a MarkerStyle instance representing the marker style of the widget when no
254
172
  # argument is given. Otherwise sets the button marker style of the widget and returns self.
255
173
  #
256
- # This method returns valid information only for check boxes and radio buttons!
174
+ # This method returns valid information only for check boxes, radio buttons and push buttons!
257
175
  #
258
- # When setting a marker style, arguments that are not provided will use the default: a black
259
- # auto-sized checkmark (i.e. :check for for check boxes) or circle (:circle for radio
260
- # buttons). This also means that multiple invocations will reset *all* prior values.
176
+ # When setting a marker style, arguments that are not provided will use the default:
177
+ #
178
+ # * For check boxes a black auto-sized checkmark (i.e. :check)
179
+ # * For radio buttons a black auto-sized circle (i.e. :circle)
180
+ # * For push buttons a black 9pt empty text using Helvetica
181
+ #
182
+ # This also means that multiple invocations will reset *all* prior values.
183
+ #
184
+ # Note that the +font_name+ argument must be a valid HexaPDF font name (this is in contrast
185
+ # to MarkerStyle#font_name which returns the resource name of the font).
261
186
  #
262
187
  # Note: The marker is called "normal caption" in the PDF 2.0 spec and the /CA entry of the
263
188
  # associated appearance characteristics dictionary. The marker size and color are set using
@@ -265,13 +190,18 @@ module HexaPDF
265
190
  # does it).
266
191
  #
267
192
  # See: PDF2.0 s12.5.6.19 and s12.7.4.3
268
- def marker_style(style: nil, size: nil, color: nil)
193
+ def marker_style(style: nil, size: nil, color: nil, font_name: nil)
269
194
  field = form_field
270
- if style || size || color
271
- style ||= (field.check_box? ? :check : :cicrle)
272
- size ||= 0
195
+ if style || size || color || font_name
196
+ style ||= case field.concrete_field_type
197
+ when :check_box then :check
198
+ when :radio_button then :circle
199
+ when :push_button then ''
200
+ end
201
+ size ||= (field.push_button? ? 9 : 0)
273
202
  color = Content::ColorSpace.device_color_from_specification(color || 0)
274
203
  serialized_color = Content::ColorSpace.serialize_device_color(color)
204
+ font_name ||= 'Helvetica'
275
205
 
276
206
  self[:MK] ||= {}
277
207
  self[:MK][:CA] = case style
@@ -285,7 +215,13 @@ module HexaPDF
285
215
  else
286
216
  raise ArgumentError, "Unknown value #{style} for argument 'style'"
287
217
  end
288
- self[:DA] = "/ZaDb #{size} Tf #{serialized_color}".strip
218
+ self[:DA] = if field.push_button?
219
+ name = document.acro_form(create: true).default_resources.
220
+ add_font(document.fonts.add(font_name).pdf_object)
221
+ "/#{name} #{size} Tf #{serialized_color}".strip
222
+ else
223
+ "/ZaDb #{size} Tf #{serialized_color}".strip
224
+ end
289
225
  else
290
226
  style = case self[:MK]&.[](:CA)
291
227
  when '4' then :check
@@ -305,12 +241,12 @@ module HexaPDF
305
241
  size = 0
306
242
  color = HexaPDF::Content::ColorSpace.prenormalized_device_color([0])
307
243
  if (da = self[:DA] || field[:DA])
308
- _, da_size, da_color = AcroForm::VariableTextField.parse_appearance_string(da)
244
+ font_name, da_size, da_color = AcroForm::VariableTextField.parse_appearance_string(da)
309
245
  size = da_size || size
310
246
  color = da_color || color
311
247
  end
312
248
 
313
- MarkerStyle.new(style, size, color)
249
+ MarkerStyle.new(style, size, color, font_name)
314
250
  end
315
251
  end
316
252
 
@@ -48,6 +48,14 @@ module HexaPDF
48
48
  autoload(:Text, 'hexapdf/type/annotations/text')
49
49
  autoload(:Link, 'hexapdf/type/annotations/link')
50
50
  autoload(:Widget, 'hexapdf/type/annotations/widget')
51
+ autoload(:BorderStyling, 'hexapdf/type/annotations/border_styling')
52
+ autoload(:Line, 'hexapdf/type/annotations/line')
53
+ autoload(:AppearanceGenerator, 'hexapdf/type/annotations/appearance_generator')
54
+ autoload(:BorderEffect, 'hexapdf/type/annotations/border_effect')
55
+ autoload(:InteriorColor, 'hexapdf/type/annotations/interior_color')
56
+ autoload(:SquareCircle, 'hexapdf/type/annotations/square_circle')
57
+ autoload(:Square, 'hexapdf/type/annotations/square')
58
+ autoload(:Circle, 'hexapdf/type/annotations/circle')
51
59
 
52
60
  end
53
61
 
@@ -167,14 +167,14 @@ module HexaPDF
167
167
  # bounding box.
168
168
  #
169
169
  # *Note* that a canvas can only be retrieved for initially empty form XObjects!
170
- def canvas
170
+ def canvas(translate: true)
171
171
  cache(:canvas) do
172
172
  unless stream.empty?
173
173
  raise HexaPDF::Error, "Cannot create a canvas for a form XObjects with contents"
174
174
  end
175
175
 
176
176
  canvas = Content::Canvas.new(self)
177
- if box.left != 0 || box.bottom != 0
177
+ if translate && (box.left != 0 || box.bottom != 0)
178
178
  canvas.save_graphics_state.translate(box.left, box.bottom)
179
179
  end
180
180
  self.stream = canvas.stream_data
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '1.1.1'
40
+ VERSION = '1.3.0'
41
41
 
42
42
  end
@@ -150,7 +150,6 @@ module HexaPDF
150
150
 
151
151
  xref_section = XRefSection.new
152
152
  xref_section.mark_as_initial_section! unless previous_xref_pos
153
- xref_section.add_free_entry(0, 65535) if previous_xref_pos.nil?
154
153
  rev.each do |obj|
155
154
  if obj.null?
156
155
  xref_section.add_free_entry(obj.oid, obj.gen)
@@ -113,9 +113,10 @@ module HexaPDF
113
113
 
114
114
  # Marks this XRefSection object as being the first cross-reference section in a PDF file.
115
115
  #
116
- # This has the consequence that only a single sub-section is created.
116
+ # This has the consequence that only a single sub-section starting a zero is created.
117
117
  def mark_as_initial_section!
118
118
  @initial_section = true
119
+ add_free_entry(0, 65535)
119
120
  end
120
121
 
121
122
  # Adds an in-use entry to the cross-reference section.
@@ -161,9 +162,10 @@ module HexaPDF
161
162
  return to_enum(__method__) unless block_given?
162
163
 
163
164
  temp = []
164
- oids.sort.each do |oid|
165
- expected_next_oid = !temp.empty? && temp[-1].oid + 1
166
- if expected_next_oid && expected_next_oid != oid
165
+ sorted_oids = oids.sort
166
+ expected_next_oid = sorted_oids[0]
167
+ sorted_oids.each do |oid|
168
+ if expected_next_oid != oid
167
169
  if @initial_section
168
170
  expected_next_oid.upto(oid - 1) do |free_oid|
169
171
  temp << self.class.free_entry(free_oid, 0)
@@ -174,6 +176,7 @@ module HexaPDF
174
176
  end
175
177
  end
176
178
  temp << self[oid]
179
+ expected_next_oid = oid + 1
177
180
  end
178
181
  yield(temp)
179
182
  self
@@ -2,7 +2,6 @@
2
2
 
3
3
  require 'test_helper'
4
4
  require 'hexapdf/content/graphics_state'
5
- require 'ostruct'
6
5
 
7
6
  # Dummy class used as wrapper so that constant lookup works correctly
8
7
  class GraphicsStateWrapper < Minitest::Spec
@@ -149,8 +148,8 @@ class GraphicsStateWrapper < Minitest::Spec
149
148
  end
150
149
 
151
150
  it "uses the correct glyph to text space scaling" do
152
- font = OpenStruct.new
153
- font.glyph_scaling_factor = 0.002
151
+ font = Object.new
152
+ font.define_singleton_method(:glyph_scaling_factor) { 0.002 }
154
153
  @gs.font = font
155
154
  @gs.font_size = 10
156
155
  assert_equal(0.02, @gs.scaled_font_size)
@@ -4,7 +4,6 @@ require 'test_helper'
4
4
  require 'hexapdf/content/operator'
5
5
  require 'hexapdf/content/processor'
6
6
  require 'hexapdf/serializer'
7
- require 'ostruct'
8
7
 
9
8
  describe HexaPDF::Content::Operator::BaseOperator do
10
9
  before do
@@ -191,8 +190,8 @@ end
191
190
 
192
191
  describe_operator :SetGraphicsStateParameters, :gs do
193
192
  it "applies parameters from an ExtGState dictionary" do
194
- font = OpenStruct.new
195
- font.glyph_scaling_factor = 0.01
193
+ font = Object.new
194
+ font.define_singleton_method(:glyph_scaling_factor) { 0.01 }
196
195
  @processor.resources[:ExtGState] = {Name: {LW: 10, LC: 2, LJ: 2, ML: 2, D: [[3, 5], 2],
197
196
  RI: 2, SA: true, BM: :Multiply, CA: 0.5, ca: 0.5,
198
197
  AIS: true, TK: false, Font: [font, 10],
@@ -453,8 +452,8 @@ describe_operator :SetFontAndSize, :Tf do
453
452
  self[:Font] && self[:Font][name]
454
453
  end
455
454
 
456
- font = OpenStruct.new
457
- font.glyph_scaling_factor = 0.01
455
+ font = Object.new
456
+ font.define_singleton_method(:glyph_scaling_factor) { 0.01 }
458
457
  @processor.resources[:Font] = {F1: font}
459
458
  invoke(:F1, 10)
460
459
  assert_equal(@processor.resources.font(:F1), @processor.graphics_state.font)
@@ -4,17 +4,16 @@ require 'digest'
4
4
  require 'test_helper'
5
5
  require_relative 'common'
6
6
  require 'hexapdf/digital_signature'
7
- require 'ostruct'
8
7
 
9
8
  describe HexaPDF::DigitalSignature::CMSHandler do
10
9
  before do
11
- @data = 'Some data'
12
- @dict = OpenStruct.new
13
- @pkcs7 = OpenSSL::PKCS7.sign(CERTIFICATES.signer_certificate, CERTIFICATES.signer_key,
14
- @data, [CERTIFICATES.ca_certificate],
15
- OpenSSL::PKCS7::DETACHED)
16
- @dict.contents = @pkcs7.to_der
17
- @dict.signed_data = @data
10
+ @data = data = 'Some data'
11
+ @dict = Struct.new(:contents, :signed_data, :signature_type, :Reference, :M).new
12
+ @pkcs7 = pkcs7 = OpenSSL::PKCS7.sign(CERTIFICATES.signer_certificate, CERTIFICATES.signer_key,
13
+ @data, [CERTIFICATES.ca_certificate],
14
+ OpenSSL::PKCS7::DETACHED)
15
+ @dict.contents = pkcs7.to_der
16
+ @dict.signed_data = data
18
17
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
19
18
  end
20
19
 
@@ -4,7 +4,6 @@ require 'test_helper'
4
4
  require 'hexapdf/digital_signature'
5
5
  require 'hexapdf/document'
6
6
  require 'time'
7
- require 'ostruct'
8
7
  require 'openssl'
9
8
 
10
9
  describe HexaPDF::DigitalSignature::Handler do
@@ -33,7 +32,7 @@ describe HexaPDF::DigitalSignature::Handler do
33
32
 
34
33
  describe "store_verification_callback" do
35
34
  before do
36
- @context = OpenStruct.new
35
+ @context = Struct.new(:error).new
37
36
  end
38
37
 
39
38
  it "can allow self-signed certificates" do
@@ -60,7 +59,7 @@ describe HexaPDF::DigitalSignature::Handler do
60
59
  ].each do |success, not_before, not_after|
61
60
  @result.messages.clear
62
61
  @handler.define_singleton_method(:signer_certificate) do
63
- OpenStruct.new.tap do |struct|
62
+ Struct.new(:not_before, :not_after).new.tap do |struct|
64
63
  struct.not_before = Time.parse("2021-11-14 #{not_before}")
65
64
  struct.not_after = Time.parse("2021-11-14 #{not_after}")
66
65
  end
@@ -3,12 +3,11 @@
3
3
  require 'test_helper'
4
4
  require_relative 'common'
5
5
  require 'hexapdf/digital_signature'
6
- require 'ostruct'
7
6
 
8
7
  describe HexaPDF::DigitalSignature::PKCS1Handler do
9
8
  before do
10
9
  @data = 'Some data'
11
- @dict = OpenStruct.new
10
+ @dict = Struct.new(:signed_data, :contents, :Cert, :Reference, :M).new
12
11
  @dict.signed_data = @data
13
12
  encoded_data = CERTIFICATES.signer_key.sign(OpenSSL::Digest.new('SHA1'), @data)
14
13
  @dict.contents = OpenSSL::ASN1::OctetString.new(encoded_data).to_der
@@ -0,0 +1,55 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+
6
+ describe HexaPDF::Document::Annotations do
7
+ before do
8
+ @doc = HexaPDF::Document.new
9
+ @page = @doc.pages.add
10
+ @annots = @doc.annotations
11
+ end
12
+
13
+ describe "create" do
14
+ it "fails if the type argument doesn't refer to an implemented method" do
15
+ assert_raises(ArgumentError) { @annots.create(:unknown, @page) }
16
+ end
17
+
18
+ it "delegates to the actual create_TYPE implementation" do
19
+ annot = @annots.create(:line, @page, start_point: [0, 0], end_point: [10, 10])
20
+ assert_equal(:Line, annot[:Subtype])
21
+ annot = @annots.create(:rectangle, @page, 10, 20, 30, 40)
22
+ assert_equal(:Square, annot[:Subtype])
23
+ end
24
+ end
25
+
26
+ describe "create_line" do
27
+ it "creates an appropriate line annotation object" do
28
+ annot = @annots.create(:line, @page, start_point: [0, 5], end_point: [10, 15])
29
+ assert_equal(:Annot, annot[:Type])
30
+ assert_equal(:Line, annot[:Subtype])
31
+ assert_equal([0, 5, 10, 15], annot.line)
32
+ assert_equal(annot, @page[:Annots].first)
33
+ end
34
+ end
35
+
36
+ describe "create_rectangle" do
37
+ it "creates an appropriate square annotation object" do
38
+ annot = @annots.create(:rectangle, @page, 10, 20, 30, 40)
39
+ assert_equal(:Annot, annot[:Type])
40
+ assert_equal(:Square, annot[:Subtype])
41
+ assert_equal([10, 20, 40, 60], annot[:Rect])
42
+ assert_equal(annot, @page[:Annots].first)
43
+ end
44
+ end
45
+
46
+ describe "create_ellipse" do
47
+ it "creates an appropriate circle annotation object" do
48
+ annot = @annots.create(:ellipse, @page, 100, 100, a: 30, b: 40)
49
+ assert_equal(:Annot, annot[:Type])
50
+ assert_equal(:Circle, annot[:Subtype])
51
+ assert_equal([70, 60, 130, 140], annot[:Rect])
52
+ assert_equal(annot, @page[:Annots].first)
53
+ end
54
+ end
55
+ end
@@ -146,6 +146,16 @@ describe HexaPDF::Document::Layout do
146
146
  end
147
147
  end
148
148
 
149
+ describe "style?" do
150
+ it "returns true if a given style is defined" do
151
+ assert(@layout.style?(:base))
152
+ end
153
+
154
+ it "returns false if a given style is not defined" do
155
+ refute(@layout.style?(:unknown))
156
+ end
157
+ end
158
+
149
159
  describe "styles" do
150
160
  it "returns the existing styles" do
151
161
  @layout.style(:test, font_size: 20)
@@ -168,6 +178,16 @@ describe HexaPDF::Document::Layout do
168
178
  assert_kind_of(HexaPDF::Font::Type1Wrapper, style.font)
169
179
  end
170
180
 
181
+ it "uses the font_bold property when resolving a font name to a font wrapper" do
182
+ style = @layout.send(:retrieve_style, {font: 'Helvetica', font_bold: true})
183
+ assert_equal('Helvetica-Bold', style.font.wrapped_font.font_name)
184
+ end
185
+
186
+ it "uses the font_italic property when resolving a font name to a font wrapper" do
187
+ style = @layout.send(:retrieve_style, {font: 'Helvetica', font_italic: true})
188
+ assert_equal('Helvetica-Oblique', style.font.wrapped_font.font_name)
189
+ end
190
+
171
191
  it "sets the :base style's font if no font is set" do
172
192
  @layout.style(:base, font: 'Helvetica')
173
193
  style = @layout.send(:retrieve_style, {})
@@ -203,7 +223,8 @@ describe HexaPDF::Document::Layout do
203
223
 
204
224
  describe "box" do
205
225
  it "creates the request box" do
206
- box = @layout.box(:column, columns: 3, gaps: 20, width: 15, height: 30, style: {font_size: 10},
226
+ box = @layout.box(:column, columns: 3, width: 15, height: 30,
227
+ style: {font_size: 10, box_options: {gaps: 20}},
207
228
  properties: {key: :value})
208
229
  assert_equal(15, box.width)
209
230
  assert_equal(30, box.height)
@@ -431,9 +452,10 @@ describe HexaPDF::Document::Layout do
431
452
 
432
453
  describe "table_box" do
433
454
  it "creates a table box" do
434
- box = @layout.table_box([['m']], column_widths: [100], header: proc { [['a']] },
455
+ box = @layout.table_box([['m']], header: proc { [['a']] },
435
456
  footer: proc { [['b']] }, cell_style: {background_color: "red"},
436
457
  width: 100, height: 300, style: {background_color: "blue"},
458
+ box_options: {column_widths: [100]},
437
459
  properties: {key: :value}, border: {width: 1})
438
460
  assert_equal(100, box.width)
439
461
  assert_equal(300, box.height)
@@ -209,6 +209,13 @@ describe HexaPDF::Font::TrueTypeWrapper do
209
209
  dict[:Encoding].stream)
210
210
  assert_equal([glyph.id, [glyph.width]], dict[:DescendantFonts][0][:W].value)
211
211
  end
212
+
213
+ it "handles the case where the font is added but then not used and deleted" do
214
+ @doc.task(:optimize, compact: true)
215
+ assert(@font_wrapper.pdf_object.null?)
216
+ @doc.dispatch_message(:complete_objects)
217
+ assert(@font_wrapper.pdf_object.null?)
218
+ end
212
219
  end
213
220
 
214
221
  describe "font file embedding" do
@@ -140,5 +140,12 @@ describe HexaPDF::Font::Type1Wrapper do
140
140
  it "makes sure that the PDF dictionaries are indirect" do
141
141
  assert(@times_wrapper.pdf_object.indirect?)
142
142
  end
143
+
144
+ it "handles the case where the font is added but then not used and deleted" do
145
+ @doc.task(:optimize, compact: true)
146
+ assert(@times_wrapper.pdf_object.null?)
147
+ @doc.dispatch_message(:complete_objects)
148
+ assert(@times_wrapper.pdf_object.null?)
149
+ end
143
150
  end
144
151
  end
@@ -27,6 +27,16 @@ describe HexaPDF::Font::TrueType::Subsetter do
27
27
  assert_equal(value, @subsetter.subset_glyph_id(5))
28
28
  end
29
29
 
30
+ it "doesn't use certain subset glyph IDs for performance reasons" do
31
+ 1.upto(93) {|i| @subsetter.use_glyph(i) }
32
+ # glyph 0, 93 used glyph, 4 special glyphs
33
+ assert_equal(1 + 93 + 4, @subsetter.instance_variable_get(:@glyph_map).size)
34
+ 1.upto(12) {|i| assert_equal(i, @subsetter.subset_glyph_id(i), "id=#{i}") }
35
+ 13.upto(38) {|i| assert_equal(i + 1, @subsetter.subset_glyph_id(i), "id=#{i}") }
36
+ 39.upto(88) {|i| assert_equal(i + 3, @subsetter.subset_glyph_id(i), "id=#{i}") }
37
+ 89.upto(93) {|i| assert_equal(i + 4, @subsetter.subset_glyph_id(i), "id=#{i}") }
38
+ end
39
+
30
40
  it "creates the subset font file" do
31
41
  gid = @font[:cmap].preferred_table[0x41]
32
42
  @subsetter.use_glyph(gid)
@@ -738,6 +738,30 @@ describe HexaPDF::Layout::Style do
738
738
  end
739
739
  end
740
740
 
741
+ describe "each_property" do
742
+ it "yields all set properties with their values" do
743
+ @style.font_size = 5
744
+ @style.line_spacing = 1.2
745
+ assert_equal(0.005, @style.scaled_font_size)
746
+ assert_equal([[:font, @style.font], [:font_size, 5], [:horizontal_scaling, 100],
747
+ [:line_spacing, @style.line_spacing], [:subscript, false], [:superscript, false]],
748
+ @style.each_property.to_a.sort)
749
+ end
750
+ end
751
+
752
+ describe "merge" do
753
+ it "merges all set properties" do
754
+ @style.font_size = 5
755
+ @style.line_spacing = 1.2
756
+ new_style = HexaPDF::Layout::Style.new
757
+ new_style.update(font_size: 3, line_spacing: {type: :fixed, value: 2.5})
758
+ new_style.merge(@style)
759
+ assert_equal(5, new_style.font_size)
760
+ assert_equal(:proportional, new_style.line_spacing.type)
761
+ assert_equal(1.2, new_style.line_spacing.value)
762
+ end
763
+ end
764
+
741
765
  it "has several simple and dynamically generated properties with default values" do
742
766
  @style = HexaPDF::Layout::Style.new
743
767
  assert_raises(HexaPDF::Error) { @style.font }
@@ -780,6 +804,7 @@ describe HexaPDF::Layout::Style do
780
804
  assert_equal(:left, @style.align)
781
805
  assert_equal(:top, @style.valign)
782
806
  assert_equal(:default, @style.mask_mode)
807
+ assert_equal({}, @style.box_options)
783
808
  end
784
809
 
785
810
  it "allows using a non-standard setter for generated properties" do
@@ -790,8 +815,8 @@ describe HexaPDF::Layout::Style do
790
815
  @style.stroke_dash_pattern(5, 2)
791
816
  assert_equal([[5], 2], @style.stroke_dash_pattern.to_operands)
792
817
 
793
- @style.line_spacing(1.2)
794
- assert_equal([:proportional, 1.2], [@style.line_spacing.type, @style.line_spacing.value])
818
+ @style.line_spacing(HexaPDF::Layout::Style::LineSpacing.new(type: :double))
819
+ assert_equal([:proportional, 2], [@style.line_spacing.type, @style.line_spacing.value])
795
820
  end
796
821
 
797
822
  it "allows checking for valid values" do
@@ -99,7 +99,7 @@ describe HexaPDF::Task::Optimize do
99
99
  objstm = @doc.add({}, type: HexaPDF::Type::ObjectStream)
100
100
  @doc.add({}, type: HexaPDF::Type::XRefStream)
101
101
  objstm.add_object(@doc.add({Type: :Test}))
102
- @doc.write(io)
102
+ @doc.write(io, compact: false)
103
103
  io.rewind
104
104
  @doc = HexaPDF::Document.new(io: io)
105
105
  end