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
@@ -0,0 +1,169 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/annotations/line'
6
+
7
+ describe HexaPDF::Type::Annotations::Line do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @line = @doc.add({Type: :Annot, Subtype: :Line, Rect: [0, 0, 0, 0], L: [0, 0, 1, 1]})
11
+ end
12
+
13
+ describe "line" do
14
+ it "returns the coordinates of the start and end points" do
15
+ @line[:L] = [10, 20, 30, 40]
16
+ assert_equal([10, 20, 30, 40], @line.line)
17
+ end
18
+
19
+ it "sets the line points" do
20
+ assert_equal(@line, @line.line(1, 2, 3, 4))
21
+ assert_equal([1, 2, 3, 4], @line[:L])
22
+ end
23
+
24
+ it "raises an ArgumentError if not all arguments are provided" do
25
+ assert_raises(ArgumentError) { @line.line(1, 2, 3) }
26
+ end
27
+ end
28
+
29
+ describe "line_ending_style" do
30
+ it "returns the current style" do
31
+ assert_kind_of(HexaPDF::Type::Annotations::Line::LineEndingStyle, @line.line_ending_style)
32
+ assert_equal([:none, :none], @line.line_ending_style.to_a)
33
+ @line[:LE] = [:Diamond, :OpenArrow]
34
+ assert_equal([:diamond, :open_arrow], @line.line_ending_style.to_a)
35
+ @line[:LE] = [:Diamond, :Unknown]
36
+ assert_equal([:diamond, :none], @line.line_ending_style.to_a)
37
+ end
38
+
39
+ it "sets the style" do
40
+ assert_same(@line, @line.line_ending_style(start_style: :OpenArrow))
41
+ assert_equal([:OpenArrow, :None], @line[:LE])
42
+ assert_same(@line, @line.line_ending_style(end_style: :open_arrow))
43
+ assert_equal([:OpenArrow, :OpenArrow], @line[:LE])
44
+ assert_same(@line, @line.line_ending_style(start_style: :circle, end_style: :ClosedArrow))
45
+ assert_equal([:Circle, :ClosedArrow], @line[:LE])
46
+ end
47
+
48
+ it "raises an error for unknown styles" do
49
+ assert_raises(ArgumentError) { @line.line_ending_style(start_style: :unknown) }
50
+ assert_raises(ArgumentError) { @line.line_ending_style(end_style: :unknown) }
51
+ end
52
+ end
53
+
54
+ describe "leader_line_length" do
55
+ it "returns the leader line length" do
56
+ assert_equal(0, @line.leader_line_length)
57
+ @line[:LL] = 10
58
+ assert_equal(10, @line.leader_line_length)
59
+ end
60
+
61
+ it "sets the leader line length" do
62
+ assert_equal(@line, @line.leader_line_length(10))
63
+ assert_equal(10, @line[:LL])
64
+ end
65
+ end
66
+
67
+ describe "leader_line_extension_length" do
68
+ it "returns the leader line extension length" do
69
+ assert_equal(0, @line.leader_line_extension_length)
70
+ @line[:LLE] = 10
71
+ assert_equal(10, @line.leader_line_extension_length)
72
+ end
73
+
74
+ it "sets the leader line extension length" do
75
+ assert_equal(@line, @line.leader_line_extension_length(10))
76
+ assert_equal(10, @line[:LLE])
77
+ end
78
+
79
+ it "raises an error for negative numbers" do
80
+ assert_raises(ArgumentError) { @line.leader_line_extension_length(-10) }
81
+ end
82
+ end
83
+
84
+ describe "leader_line_offset" do
85
+ it "returns the leader line offset" do
86
+ assert_equal(0, @line.leader_line_offset)
87
+ @line[:LLO] = 10
88
+ assert_equal(10, @line.leader_line_offset)
89
+ end
90
+
91
+ it "sets the leader line offset" do
92
+ assert_equal(@line, @line.leader_line_offset(10))
93
+ assert_equal(10, @line[:LLO])
94
+ end
95
+ end
96
+
97
+ describe "captioned" do
98
+ it "returns whether a caption is shown" do
99
+ refute(@line.captioned)
100
+ @line[:Cap] = true
101
+ assert(@line.captioned)
102
+ end
103
+
104
+ it "sets whether a caption should be shown" do
105
+ assert_equal(@line, @line.captioned(true))
106
+ assert_equal(true, @line[:Cap])
107
+ end
108
+ end
109
+
110
+ describe "caption_position" do
111
+ it "returns the caption position" do
112
+ assert_equal(:inline, @line.caption_position)
113
+ @line[:CP] = :Top
114
+ assert_equal(:top, @line.caption_position)
115
+ end
116
+
117
+ it "sets the caption position" do
118
+ assert_equal(@line, @line.caption_position(:top))
119
+ assert_equal(:Top, @line[:CP])
120
+ end
121
+
122
+ it "raises an error on invalid caption positions" do
123
+ assert_raises(ArgumentError) { @line.caption_position(:unknown) }
124
+ end
125
+ end
126
+
127
+ describe "caption_offset" do
128
+ it "returns the caption offset" do
129
+ assert_equal([0, 0], @line.caption_offset)
130
+ @line[:CO] = [10, 5]
131
+ assert_equal([10, 5], @line.caption_offset)
132
+ end
133
+
134
+ it "sets the caption offset" do
135
+ assert_equal(@line, @line.caption_offset(5, 10))
136
+ assert_equal([5, 10], @line[:CO])
137
+ assert_equal(@line, @line.caption_offset(5))
138
+ assert_equal([5, 0], @line[:CO])
139
+ assert_equal(@line, @line.caption_offset(nil, 5))
140
+ assert_equal([0, 5], @line[:CO])
141
+ end
142
+ end
143
+
144
+ describe "perform_validation" do
145
+ it "validates that leader line length is set if extension length is set" do
146
+ @line[:LLE] = 10
147
+ m = nil
148
+ refute(@line.validate {|msg| m = msg })
149
+ assert_match(/\/LL required to be non-zero/, m)
150
+ end
151
+
152
+ it "ensures leader line extension length is non-negative" do
153
+ @line[:LL] = 10
154
+ @line[:LLE] = -10
155
+ m = nil
156
+ assert(@line.validate {|msg| m = msg })
157
+ assert_equal(10, @line[:LLE])
158
+ assert_match(/non-negative/, m)
159
+ end
160
+
161
+ it "ensures leader line offset is non-negative" do
162
+ @line[:LLO] = -10
163
+ m = nil
164
+ assert(@line.validate {|msg| m = msg })
165
+ assert_equal(10, @line[:LLO])
166
+ assert_match(/\/LLO must be a non-negative/, m)
167
+ end
168
+ end
169
+ end
@@ -69,87 +69,6 @@ describe HexaPDF::Type::Annotations::Widget do
69
69
  end
70
70
  end
71
71
 
72
- describe "border_style" do
73
- before do
74
- @widget[:MK] = {BC: [1, 0, 1]}
75
- @color = HexaPDF::Content::ColorSpace.prenormalized_device_color([1, 0, 1])
76
- end
77
-
78
- describe "getter" do
79
- it "no /Border, /BS or /MK set" do
80
- @widget.delete(:MK)
81
- assert_equal([1, nil, :solid, 0, 0], @widget.border_style.to_a)
82
- end
83
-
84
- it "no /Border, /BS but with /MK empty" do
85
- @widget[:MK].delete(:BC)
86
- assert_equal([1, nil, :solid, 0, 0], @widget.border_style.to_a)
87
- end
88
-
89
- it "uses the color from /MK" do
90
- assert_equal([1, @color, :solid, 0, 0], @widget.border_style.to_a)
91
- @widget[:MK][:BC] = []
92
- assert_equal([1, nil, :solid, 0, 0], @widget.border_style.to_a)
93
- end
94
-
95
- it "uses the data from /Border" do
96
- @widget[:Border] = [1, 2, 3, [1, 2]]
97
- assert_equal([3, @color, [1, 2], 1, 2], @widget.border_style.to_a)
98
- end
99
-
100
- it "uses the data from /BS, overriding /Border values" do
101
- @widget[:Border] = [1, 2, 3, [1, 2]]
102
- @widget[:BS] = {W: 5, S: :D, D: [5, 6]}
103
- assert_equal([5, @color, [5, 6], 0, 0], @widget.border_style.to_a)
104
-
105
- [[:S, :solid], [:D, [5, 6]], [:B, :beveled], [:I, :inset],
106
- [:U, :underlined], [:Unknown, :solid]].each do |val, result|
107
- @widget[:BS] = {S: val, D: [5, 6]}
108
- assert_equal([1, @color, result, 0, 0], @widget.border_style.to_a)
109
- end
110
- end
111
- end
112
-
113
- describe "setter" do
114
- it "returns self" do
115
- assert_equal(@widget, @widget.border_style(width: 1))
116
- end
117
-
118
- it "sets the color" do
119
- @widget.border_style(color: [1.0, 51, 1.0])
120
- assert_equal([1, 0.2, 1], @widget[:MK][:BC].value)
121
-
122
- @widget.border_style(color: :transparent)
123
- assert_equal([], @widget[:MK][:BC].value)
124
- end
125
-
126
- it "sets the width" do
127
- @widget.border_style(width: 2)
128
- assert_equal(2, @widget[:BS][:W])
129
- end
130
-
131
- it "sets the style" do
132
- [[:solid, :S], [[5, 6], :D], [:beveled, :B], [:inset, :I], [:underlined, :U]].each do |val, r|
133
- @widget.border_style(style: val)
134
- assert_equal(r, @widget[:BS][:S])
135
- assert_equal(val, @widget[:BS][:D].value) if r == :D
136
- end
137
- end
138
-
139
- it "overrides all priorly set values" do
140
- @widget.border_style(width: 3, style: :inset, color: [1])
141
- @widget.border_style(width: 5)
142
- border_style = @widget.border_style
143
- assert_equal(:solid, border_style.style)
144
- assert_equal([0], border_style.color.components)
145
- end
146
-
147
- it "raises an error for an unknown style" do
148
- assert_raises(ArgumentError) { @widget.border_style(style: :unknown) }
149
- end
150
- end
151
- end
152
-
153
72
  describe "marker_style" do
154
73
  before do
155
74
  @chars = %w[4 l 8 u n H S]
@@ -179,6 +98,20 @@ describe HexaPDF::Type::Annotations::Widget do
179
98
  end
180
99
  end
181
100
 
101
+ it "uses the correct default style" do
102
+ @widget.form_field.initialize_as_check_box
103
+ @widget.marker_style(size: 10)
104
+ assert_equal('4', @widget[:MK][:CA])
105
+
106
+ @widget.form_field.initialize_as_radio_button
107
+ @widget.marker_style(size: 10)
108
+ assert_equal('l', @widget[:MK][:CA])
109
+
110
+ @widget.form_field.initialize_as_push_button
111
+ @widget.marker_style(size: 10)
112
+ assert_equal('', @widget[:MK][:CA])
113
+ end
114
+
182
115
  it "fails if an invalid argument is provided" do
183
116
  assert_raises(ArgumentError) { @widget.marker_style(style: 5) }
184
117
  end
@@ -225,5 +158,26 @@ describe HexaPDF::Type::Annotations::Widget do
225
158
  assert_equal([1, 0.2, 1, 1], @widget.marker_style.color.components)
226
159
  end
227
160
  end
161
+
162
+ describe "font_name" do
163
+ it "returns the font_name" do
164
+ @widget.form_field[:DA] = "/F1 15 Tf"
165
+ assert_equal(:F1, @widget.marker_style.font_name)
166
+ @widget[:DA] = "/F2 10 Tf"
167
+ assert_equal(:F2, @widget.marker_style.font_name)
168
+ end
169
+
170
+ it "returns nil if none is set" do
171
+ assert_nil(@widget.marker_style.font_name)
172
+ @widget.form_field[:DA] = "0.0 g"
173
+ assert_nil(@widget.marker_style.font_name)
174
+ end
175
+
176
+ it "sets the given font_name" do
177
+ @widget.form_field.initialize_as_push_button
178
+ @widget.marker_style(font_name: 'Helvetica', size: 10)
179
+ assert_equal('/F1 10 Tf 0.0 g', @widget[:DA])
180
+ end
181
+ end
228
182
  end
229
183
  end
@@ -113,6 +113,14 @@ describe HexaPDF::Type::Annotation do
113
113
  end
114
114
  end
115
115
 
116
+ describe "regenerate_appearance" do
117
+ it "regenerates the appearance using the data from the annotation object" do
118
+ @annot[:Subtype] = :Unknown
119
+ error = assert_raises(HexaPDF::Error) { @annot.regenerate_appearance }
120
+ assert_match(/Unknown.*not.*supported/, error.message)
121
+ end
122
+ end
123
+
116
124
  describe "flags" do
117
125
  it "returns all flags" do
118
126
  assert_equal([:invisible, :hidden, :no_view], @annot.flags)
@@ -139,6 +147,53 @@ describe HexaPDF::Type::Annotation do
139
147
  end
140
148
  end
141
149
 
150
+ describe "contents" do
151
+ it "returns the contents value" do
152
+ assert_nil(@annot.contents)
153
+ @annot[:Contents] = "test"
154
+ assert_equal("test", @annot.contents)
155
+ end
156
+
157
+ it "sets the contents value" do
158
+ assert_same(@annot, @annot.contents("Test"))
159
+ assert_equal("Test", @annot[:Contents])
160
+ @annot.contents(nil)
161
+ assert_nil(@annot[:Contents])
162
+ end
163
+ end
164
+
165
+ describe "opacity" do
166
+ it "returns the opacity values" do
167
+ opacity = @annot.opacity
168
+ assert_equal(1, opacity.fill_alpha)
169
+ assert_equal(1, opacity.stroke_alpha)
170
+
171
+ @annot[:CA] = 0.5
172
+ opacity = @annot.opacity
173
+ assert_equal(0.5, opacity.fill_alpha)
174
+ assert_equal(0.5, opacity.stroke_alpha)
175
+
176
+ @annot[:ca] = 0.3
177
+ opacity = @annot.opacity
178
+ assert_equal(0.3, opacity.fill_alpha)
179
+ assert_equal(0.5, opacity.stroke_alpha)
180
+ end
181
+
182
+ it "sets the opacity values" do
183
+ @annot.opacity(fill_alpha: 0.3)
184
+ refute(@annot.key?(:CA))
185
+ assert_equal(0.3, @annot[:ca])
186
+
187
+ @annot.opacity(stroke_alpha: 0.5)
188
+ assert_equal(0.3, @annot[:ca])
189
+ assert_equal(0.5, @annot[:CA])
190
+
191
+ @annot.opacity(stroke_alpha: 0.1, fill_alpha: 0.2)
192
+ assert_equal(0.1, @annot[:CA])
193
+ assert_equal(0.2, @annot[:ca])
194
+ end
195
+ end
196
+
142
197
  describe "validation" do
143
198
  it "makes sure that empty appearance stream dictionaries don't cause validation errors" do
144
199
  assert(@annot.validate)
@@ -125,6 +125,12 @@ describe HexaPDF::Type::Form do
125
125
  [:restore_graphics_state]])
126
126
  end
127
127
 
128
+ it "doesn't move the origin if translate is false" do
129
+ @form[:BBox] = [-10, -5, 100, 300]
130
+ @form.canvas(translate: false).line_width = 5
131
+ assert_operators(@form, [[:set_line_width, [5]]])
132
+ end
133
+
128
134
  it "fails if the form XObject already has data" do
129
135
  @form.stream = '10 w'
130
136
  assert_raises(HexaPDF::Error) { @form.canvas }
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.1
4
+ version: 1.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-01-08 00:00:00.000000000 Z
10
+ date: 2025-04-23 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: cmdparse
@@ -371,6 +371,7 @@ files:
371
371
  - lib/hexapdf/digital_signature/signing/timestamp_handler.rb
372
372
  - lib/hexapdf/digital_signature/verification_result.rb
373
373
  - lib/hexapdf/document.rb
374
+ - lib/hexapdf/document/annotations.rb
374
375
  - lib/hexapdf/document/destinations.rb
375
376
  - lib/hexapdf/document/files.rb
376
377
  - lib/hexapdf/document/fonts.rb
@@ -507,8 +508,16 @@ files:
507
508
  - lib/hexapdf/type/actions/uri.rb
508
509
  - lib/hexapdf/type/annotation.rb
509
510
  - lib/hexapdf/type/annotations.rb
511
+ - lib/hexapdf/type/annotations/appearance_generator.rb
512
+ - lib/hexapdf/type/annotations/border_effect.rb
513
+ - lib/hexapdf/type/annotations/border_styling.rb
514
+ - lib/hexapdf/type/annotations/circle.rb
515
+ - lib/hexapdf/type/annotations/interior_color.rb
516
+ - lib/hexapdf/type/annotations/line.rb
510
517
  - lib/hexapdf/type/annotations/link.rb
511
518
  - lib/hexapdf/type/annotations/markup_annotation.rb
519
+ - lib/hexapdf/type/annotations/square.rb
520
+ - lib/hexapdf/type/annotations/square_circle.rb
512
521
  - lib/hexapdf/type/annotations/text.rb
513
522
  - lib/hexapdf/type/annotations/widget.rb
514
523
  - lib/hexapdf/type/catalog.rb
@@ -661,6 +670,7 @@ files:
661
670
  - test/hexapdf/digital_signature/test_signatures.rb
662
671
  - test/hexapdf/digital_signature/test_signing.rb
663
672
  - test/hexapdf/digital_signature/test_verification_result.rb
673
+ - test/hexapdf/document/test_annotations.rb
664
674
  - test/hexapdf/document/test_destinations.rb
665
675
  - test/hexapdf/document/test_files.rb
666
676
  - test/hexapdf/document/test_fonts.rb
@@ -785,6 +795,11 @@ files:
785
795
  - test/hexapdf/type/actions/test_launch.rb
786
796
  - test/hexapdf/type/actions/test_set_ocg_state.rb
787
797
  - test/hexapdf/type/actions/test_uri.rb
798
+ - test/hexapdf/type/annotations/test_appearance_generator.rb
799
+ - test/hexapdf/type/annotations/test_border_effect.rb
800
+ - test/hexapdf/type/annotations/test_border_styling.rb
801
+ - test/hexapdf/type/annotations/test_interior_color.rb
802
+ - test/hexapdf/type/annotations/test_line.rb
788
803
  - test/hexapdf/type/annotations/test_markup_annotation.rb
789
804
  - test/hexapdf/type/annotations/test_text.rb
790
805
  - test/hexapdf/type/annotations/test_widget.rb