hexapdf 1.1.1 → 1.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +34 -0
  3. data/lib/hexapdf/cli/command.rb +63 -63
  4. data/lib/hexapdf/cli/inspect.rb +1 -1
  5. data/lib/hexapdf/cli/modify.rb +0 -1
  6. data/lib/hexapdf/cli/optimize.rb +5 -5
  7. data/lib/hexapdf/configuration.rb +21 -0
  8. data/lib/hexapdf/content/graphics_state.rb +1 -1
  9. data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
  10. data/lib/hexapdf/document/annotations.rb +115 -0
  11. data/lib/hexapdf/document.rb +28 -7
  12. data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
  13. data/lib/hexapdf/font/type1_wrapper.rb +1 -0
  14. data/lib/hexapdf/type/acro_form/java_script_actions.rb +9 -2
  15. data/lib/hexapdf/type/acro_form/text_field.rb +9 -2
  16. data/lib/hexapdf/type/annotation.rb +59 -1
  17. data/lib/hexapdf/type/annotations/appearance_generator.rb +273 -0
  18. data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
  19. data/lib/hexapdf/type/annotations/line.rb +521 -0
  20. data/lib/hexapdf/type/annotations/widget.rb +2 -96
  21. data/lib/hexapdf/type/annotations.rb +3 -0
  22. data/lib/hexapdf/type/form.rb +2 -2
  23. data/lib/hexapdf/version.rb +1 -1
  24. data/lib/hexapdf/writer.rb +0 -1
  25. data/lib/hexapdf/xref_section.rb +7 -4
  26. data/test/hexapdf/content/test_graphics_state.rb +2 -3
  27. data/test/hexapdf/content/test_operator.rb +4 -5
  28. data/test/hexapdf/digital_signature/test_cms_handler.rb +7 -8
  29. data/test/hexapdf/digital_signature/test_handler.rb +2 -3
  30. data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +1 -2
  31. data/test/hexapdf/document/test_annotations.rb +33 -0
  32. data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
  33. data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
  34. data/test/hexapdf/task/test_optimize.rb +1 -1
  35. data/test/hexapdf/test_document.rb +11 -3
  36. data/test/hexapdf/test_stream.rb +1 -2
  37. data/test/hexapdf/test_xref_section.rb +1 -1
  38. data/test/hexapdf/type/acro_form/test_java_script_actions.rb +21 -0
  39. data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
  40. data/test/hexapdf/type/annotations/test_appearance_generator.rb +398 -0
  41. data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
  42. data/test/hexapdf/type/annotations/test_line.rb +189 -0
  43. data/test/hexapdf/type/annotations/test_widget.rb +0 -81
  44. data/test/hexapdf/type/test_annotation.rb +55 -0
  45. data/test/hexapdf/type/test_form.rb +6 -0
  46. metadata +10 -2
@@ -0,0 +1,521 @@
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-2025 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
+
37
+ require 'hexapdf/type/annotation'
38
+
39
+ module HexaPDF
40
+ module Type
41
+ module Annotations
42
+
43
+ # A line annotation is a markup annotation that displays a single straight line.
44
+ #
45
+ # The style of the line annotation, like adding leader lines, changing the colors and so on,
46
+ # can be customized using the provided convenience methods.
47
+ #
48
+ # Note that changing the line width and color is done using the included
49
+ # BorderStyling#border_style. While that method allows special styling of the line (like
50
+ # :beveled), only a simple line dash pattern is supported by the line annotation.
51
+ #
52
+ # Example:
53
+ #
54
+ # #>pdf-small
55
+ # doc.annotations.create_line(doc.pages[0], start_point: [30, 20], end_point: [90, 60]).
56
+ # border_style(color: "hp-blue", width: 2, style: [3, 1]).
57
+ # leader_line_length(15).
58
+ # leader_line_extension_length(10).
59
+ # leader_line_offset(5).
60
+ # interior_color("hp-orange").
61
+ # line_ending_style(start_style: :circle, end_style: :open_arrow).
62
+ # captioned(true).
63
+ # contents("Caption").
64
+ # caption_position(:top).
65
+ # caption_offset(0, 5).
66
+ # regenerate_appearance
67
+ # canvas.line(30, 20, 90, 60).stroke
68
+ #
69
+ # See: PDF2.0 s12.5.6.7, HexaPDF::Type::MarkupAnnotation
70
+ class Line < MarkupAnnotation
71
+
72
+ include BorderStyling
73
+
74
+ define_field :Subtype, type: Symbol, required: true, default: :Line
75
+ define_field :L, type: PDFArray, required: true
76
+ define_field :BS, type: :Border
77
+ define_field :LE, type: PDFArray, default: [:None, :None], version: '1.4'
78
+ define_field :IC, type: PDFArray, version: '1.4'
79
+ define_field :LL, type: Numeric, default: 0, version: '1.6'
80
+ define_field :LLE, type: Numeric, default: 0, version: '1.6'
81
+ define_field :Cap, type: Boolean, default: false, version: '1.6'
82
+ define_field :IT, type: Symbol, version: '1.6',
83
+ allowed_values: [:LineArrow, :LineDimension]
84
+ define_field :LLO, type: Numeric, version: '1.7'
85
+ define_field :CP, type: Symbol, default: :Inline, version: '1.7',
86
+ allowed_values: [:Inline, :Top]
87
+ define_field :Measure, type: Dictionary, version: '1.7'
88
+ define_field :CO, type: PDFArray, default: [0, 0], version: '1.7'
89
+
90
+ # :call-seq:
91
+ # line.line => [x0, y0, x1, y1]
92
+ # line.line(x0, y0, x1, y1) => line
93
+ #
94
+ # Returns the start point and end point of the line as an array of four numbers [x0, y0, x1,
95
+ # y1] when no argument is given. Otherwise sets the start and end point of the line and
96
+ # returns self.
97
+ #
98
+ # This is the only required setting for a line annotation. Note, however, that without
99
+ # setting an appropriate color through #border_style the line will be transparent.
100
+ #
101
+ # Example:
102
+ #
103
+ # #>pdf-small
104
+ # doc.annotations.
105
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
106
+ # regenerate_appearance
107
+ def line(x0 = nil, y0 = nil, x1 = nil, y1 = nil)
108
+ if x0.nil? && y0.nil? && x1.nil? && y1.nil?
109
+ self[:L].to_ary
110
+ elsif !x0 || !y0 || !x1 || !y1
111
+ raise ArgumentError, "All four arguments x0, y0, x1, y1 must be provided"
112
+ else
113
+ self[:L] = [x0, y0, x1, y1]
114
+ self
115
+ end
116
+ end
117
+
118
+ # Maps HexaPDF names to PDF names.
119
+ LINE_ENDING_STYLE_MAP = { # :nodoc:
120
+ Square: :Square, square: :Square,
121
+ Circle: :Circle, circle: :Circle,
122
+ Diamond: :Diamond, diamond: :Diamond,
123
+ OpenArrow: :OpenArrow, open_arrow: :OpenArrow,
124
+ ClosedArrow: :ClosedArrow, closed_arrow: :ClosedArrow,
125
+ None: :None, none: :None,
126
+ Butt: :Butt, butt: :Butt,
127
+ ROpenArrow: :ROpenArrow, ropen_arrow: :ROpenArrow,
128
+ RClosedArrow: :RClosedArrow, rclosed_arrow: :RClosedArrow,
129
+ Slash: :Slash, slash: :Slash,
130
+ }.freeze
131
+ LINE_ENDING_STYLE_REVERSE_MAP = LINE_ENDING_STYLE_MAP.invert # :nodoc:
132
+
133
+
134
+ # Describes the line ending style for a line annotation, i.e. the +start_style+ and the
135
+ # +end_style+.
136
+ #
137
+ # See Line#line_ending_style for more information.
138
+ LineEndingStyle = Struct.new(:start_style, :end_style)
139
+
140
+ # :call-seq:
141
+ # line.line_ending_style => style
142
+ # line.line_ending_style(start_style: :none, end_style: :none) => line
143
+ #
144
+ # Returns a LineEndingStyle instance holding the current line ending styles when no argument
145
+ # is given. Otherwise sets the line ending style of the line and returns self.
146
+ #
147
+ # When returning the styles, unknown line ending styles are mapped to :none.
148
+ #
149
+ # When setting the line ending style, arguments that are not provided will use the currently
150
+ # defined value or fall back to the default of +:none+.
151
+ #
152
+ # Possible line ending styles (the first one is the HexaPDF name, the second the PDF name):
153
+ #
154
+ # :square or :Square::
155
+ # A square filled with the annotation's interior colour, if any.
156
+ #
157
+ # #>pdf-small-hide
158
+ # doc.annotations.
159
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
160
+ # interior_color("hp-orange").
161
+ # line_ending_style(end_style: :square).
162
+ # regenerate_appearance
163
+ #
164
+ # :circle or :Circle::
165
+ # A circle filled with the annotation’s interior colour, if any.
166
+ #
167
+ # #>pdf-small-hide
168
+ # doc.annotations.
169
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
170
+ # interior_color("hp-orange").
171
+ # line_ending_style(end_style: :circle).
172
+ # regenerate_appearance
173
+ #
174
+ # :diamond or :Diamond::
175
+ # A diamond shape filled with the annotation’s interior colour, if any.
176
+ #
177
+ # #>pdf-small-hide
178
+ # doc.annotations.
179
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
180
+ # interior_color("hp-orange").
181
+ # line_ending_style(end_style: :diamond).
182
+ # regenerate_appearance
183
+ #
184
+ # :open_arrow or :OpenArrow::
185
+ # Two short lines meeting in an acute angle to form an open arrowhead.
186
+ #
187
+ # #>pdf-small-hide
188
+ # doc.annotations.
189
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
190
+ # interior_color("hp-orange").
191
+ # line_ending_style(end_style: :open_arrow).
192
+ # regenerate_appearance
193
+ #
194
+ # :closed_arrow or :ClosedArrow::
195
+ # Two short lines meeting in an acute angle as in the +:open_arrow+ style and connected
196
+ # by a third line to form a triangular closed arrowhead filled with the annotation’s
197
+ # interior colour, if any.
198
+ #
199
+ # #>pdf-small-hide
200
+ # doc.annotations.
201
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
202
+ # interior_color("hp-orange").
203
+ # line_ending_style(end_style: :closed_arrow).
204
+ # regenerate_appearance
205
+ #
206
+ # :none or :None::
207
+ # No line ending.
208
+ #
209
+ # #>pdf-small-hide
210
+ # doc.annotations.
211
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
212
+ # interior_color("hp-orange").
213
+ # line_ending_style(end_style: :none).
214
+ # regenerate_appearance
215
+ #
216
+ # :butt or :Butt::
217
+ # A short line at the endpoint perpendicular to the line itself.
218
+ #
219
+ # #>pdf-small-hide
220
+ # doc.annotations.
221
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
222
+ # interior_color("hp-orange").
223
+ # line_ending_style(end_style: :butt).
224
+ # regenerate_appearance
225
+ #
226
+ # :ropen_arrow or :ROpenArrow::
227
+ # Two short lines in the reverse direction from +:open_arrow+.
228
+ #
229
+ # #>pdf-small-hide
230
+ # doc.annotations.
231
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
232
+ # interior_color("hp-orange").
233
+ # line_ending_style(end_style: :ropen_arrow).
234
+ # regenerate_appearance
235
+ #
236
+ # :rclosed_arrow or :RClosedArrow::
237
+ # A triangular closed arrowhead in the reverse direction from +:closed_arrow+.
238
+ #
239
+ # #>pdf-small-hide
240
+ # doc.annotations.
241
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
242
+ # interior_color("hp-orange").
243
+ # line_ending_style(end_style: :rclosed_arrow).
244
+ # regenerate_appearance
245
+ #
246
+ # :slash or :Slash::
247
+ # A short line at the endpoint approximately 30 degrees clockwise from perpendicular to
248
+ # the line itself.
249
+ #
250
+ # #>pdf-small-hide
251
+ # doc.annotations.
252
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
253
+ # interior_color("hp-orange").
254
+ # line_ending_style(end_style: :slash).
255
+ # regenerate_appearance
256
+ def line_ending_style(start_style: :UNSET, end_style: :UNSET)
257
+ if start_style == :UNSET && end_style == :UNSET
258
+ le = self[:LE]
259
+ LineEndingStyle.new(LINE_ENDING_STYLE_REVERSE_MAP.fetch(le[0], :none),
260
+ LINE_ENDING_STYLE_REVERSE_MAP.fetch(le[1], :none))
261
+ else
262
+ start_style = self[:LE][0] if start_style == :UNSET
263
+ end_style = self[:LE][1] if end_style == :UNSET
264
+ start_style = LINE_ENDING_STYLE_MAP.fetch(start_style) do
265
+ raise ArgumentError, "Invalid line ending style: #{start_style.inspect}"
266
+ end
267
+ end_style = LINE_ENDING_STYLE_MAP.fetch(end_style) do
268
+ raise ArgumentError, "Invalid line ending style: #{end_style.inspect}"
269
+ end
270
+ self[:LE] = [start_style, end_style]
271
+ self
272
+ end
273
+ end
274
+
275
+ # :call-seq:
276
+ # line.interior_color => color or nil
277
+ # line.interior_color(*color) => line
278
+ #
279
+ # Returns the interior color or +nil+ (in case the interior color should be transparent)
280
+ # when no argument is given. Otherwise sets the interior color and returns self.
281
+ #
282
+ # The interior color is used to fill the line endings depending on the line ending styles.
283
+ #
284
+ # +color+:: The interior color. See
285
+ # HexaPDF::Content::ColorSpace.device_color_from_specification for information on
286
+ # the allowed arguments.
287
+ #
288
+ # If the special value +:transparent+ is used when setting the color, no color is
289
+ # used for filling the line endings.
290
+ #
291
+ # Also see: #line_ending_style
292
+ def interior_color(*color)
293
+ if color.empty?
294
+ color = self[:IC]
295
+ color && !color.empty? ? Content::ColorSpace.prenormalized_device_color(color.value) : nil
296
+ else
297
+ color = if color.length == 1 && color.first == :transparent
298
+ []
299
+ else
300
+ Content::ColorSpace.device_color_from_specification(color).components
301
+ end
302
+ self[:IC] = color
303
+ self
304
+ end
305
+ end
306
+
307
+ # :call-seq:
308
+ # line.leader_line_length => leader_line_length
309
+ # line.leader_line_length(length) => line
310
+ #
311
+ # Returns the leader line length when no argument is given. Otherwise sets the leader line
312
+ # length and returns self.
313
+ #
314
+ # Leader lines extend from the line's end points perpendicular to the line. If the length
315
+ # value is positive, the leader lines appear in the clockwise direction, otherwise in the
316
+ # opposite direction.
317
+ #
318
+ # Note: The "line's end points" mean the actually drawn line and not the one specified with
319
+ # #line as those two are different when leader lines are involved.
320
+ #
321
+ # A value of zero means that no leader lines are used.
322
+ #
323
+ # Example:
324
+ #
325
+ # #>pdf-small
326
+ # doc.annotations.
327
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
328
+ # leader_line_length(15).
329
+ # regenerate_appearance
330
+ # canvas.stroke_color("hp-orange").line(20, 20, 80, 60).stroke
331
+ #
332
+ # Also see: #leader_line_extension_length, #leader_line_offset
333
+ def leader_line_length(length = nil)
334
+ length ? (self[:LL] = length; self) : self[:LL]
335
+ end
336
+
337
+ # :call-seq:
338
+ # line.leader_line_extension_length => leader_line_extension_length
339
+ # line.leader_line_extension_length(length) => line
340
+ #
341
+ # Returns the leader line extension length when no argument is given. Otherwise sets the
342
+ # leader line extension length and returns self.
343
+ #
344
+ # Leader line extensions extend from the line into the opposite direction of the leader
345
+ # lines.
346
+ #
347
+ # The argument +length+ must be non-negative.
348
+ #
349
+ # If the leader line extension length is set to a positive value, the leader line length
350
+ # also needs to be specified.
351
+ #
352
+ # Example:
353
+ #
354
+ # #>pdf-small
355
+ # doc.annotations.
356
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
357
+ # leader_line_length(15).
358
+ # leader_line_extension_length(5).
359
+ # regenerate_appearance
360
+ # canvas.stroke_color("hp-orange").line(20, 20, 80, 60).stroke
361
+ #
362
+ # Also see: #leader_line_length, #leader_line_offset
363
+ def leader_line_extension_length(length = nil)
364
+ if length
365
+ raise ArgumentError, "length must be non-negative" if length < 0
366
+ self[:LLE] = length
367
+ self
368
+ else
369
+ self[:LLE]
370
+ end
371
+ end
372
+
373
+ # :call-seq:
374
+ # line.leader_line_offset => leader_line_offset
375
+ # line.leader_line_offset(number) => line
376
+ #
377
+ # Returns the leader line offset when no argument is given. Otherwise sets the leader line
378
+ # offset and returns self.
379
+ #
380
+ # The leader line offset is a non-negative number that describes the offset of the leader
381
+ # lines from the endpoints of the line.
382
+ #
383
+ # Example:
384
+ #
385
+ # #>pdf-small
386
+ # doc.annotations.
387
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
388
+ # leader_line_length(15).
389
+ # leader_line_offset(5).
390
+ # regenerate_appearance
391
+ # canvas.stroke_color("hp-orange").line(20, 20, 80, 60).stroke
392
+ #
393
+ # Also see: #leader_line_length, #leader_line_extension_length
394
+ def leader_line_offset(offset = nil)
395
+ offset ? (self[:LLO] = offset; self) : self[:LLO] || 0
396
+ end
397
+
398
+ # :call-seq:
399
+ # line.captioned => true or false
400
+ # line.captioned(value) => line
401
+ #
402
+ # Returns +true+ (if the line has a visible caption) or +false+ (no visible caption) when no
403
+ # argument is given. Otherwise sets whether a caption should be visible and returns self.
404
+ #
405
+ # If a caption should be shown, the text specified by the /Contents or /RC entries is shown
406
+ # in the appearance of the line.
407
+ #
408
+ # Example:
409
+ #
410
+ # #>pdf-small-hide
411
+ # doc.annotations.
412
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
413
+ # contents("Inline text").
414
+ # captioned(true).
415
+ # regenerate_appearance
416
+ # Also see: #caption_position, #caption_offset
417
+ def captioned(value = nil)
418
+ value ? (self[:Cap] = value; self) : self[:Cap]
419
+ end
420
+
421
+ # Maps HexaPDF names to PDF names.
422
+ CAPTION_POSITION_MAP = { # :nodoc:
423
+ Inline: :Inline, inline: :Inline,
424
+ Top: :Top, top: :Top,
425
+ }.freeze
426
+ CAPTION_POSITION_REVERSE_MAP = CAPTION_POSITION_MAP.invert # :nodoc:
427
+
428
+ # :call-seq:
429
+ # line.caption_position => caption_position
430
+ # line.caption_position(value) => line
431
+ #
432
+ # Returns the caption position when no argument is given. Otherwise sets the caption
433
+ # position and returns self.
434
+ #
435
+ # Possible caption positions are (the first one is the HexaPDF name, the second the PDF
436
+ # name):
437
+ #
438
+ # :inline or :Inline::
439
+ # The caption is centered inside the line (default).
440
+ #
441
+ # #>pdf-small-hide
442
+ # doc.annotations.
443
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
444
+ # contents("Inline text").
445
+ # captioned(true).
446
+ # caption_position(:inline).
447
+ # regenerate_appearance
448
+ #
449
+ # :top or :Top::
450
+ # The caption is on the top of the line.
451
+ #
452
+ # #>pdf-small-hide
453
+ # doc.annotations.
454
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
455
+ # contents("Top text").
456
+ # captioned(true).
457
+ # caption_position(:top).
458
+ # regenerate_appearance
459
+ #
460
+ # Also see: #captioned, #caption_offset
461
+ def caption_position(value = nil)
462
+ if value
463
+ value = CAPTION_POSITION_MAP.fetch(value) do
464
+ raise ArgumentError, "Invalid caption position: #{value.inspect}"
465
+ end
466
+ self[:CP] = value
467
+ self
468
+ else
469
+ CAPTION_POSITION_REVERSE_MAP[self[:CP]]
470
+ end
471
+ end
472
+
473
+ # :call-seq:
474
+ # line.caption_offset => caption_offset
475
+ # line.caption_offset(x, y) => line
476
+ #
477
+ # Returns the caption offset when no argument is given. Otherwise sets the caption offset
478
+ # and returns self.
479
+ #
480
+ # The caption offset is an array of two numbers that specify the horizontal and vertical
481
+ # offsets of the caption from its normal position. A positive horizontal offset means moving
482
+ # the caption to the right. A positive vertical offset means shifting the caption up.
483
+ #
484
+ # Example:
485
+ #
486
+ # #>pdf-small-hide
487
+ # doc.annotations.
488
+ # create_line(doc.pages[0], start_point: [20, 20], end_point: [80, 60]).
489
+ # contents("Top text").
490
+ # captioned(true).
491
+ # caption_position(:top).
492
+ # caption_offset(20, 10).
493
+ # regenerate_appearance
494
+ #
495
+ # Also see: #captioned, #caption_position
496
+ def caption_offset(x = nil, y = nil)
497
+ x || y ? (self[:CO] = [x || 0, y || 0]; self) : self[:CO].to_ary
498
+ end
499
+
500
+ private
501
+
502
+ def perform_validation #:nodoc:
503
+ super
504
+ if self[:LLE] < 0
505
+ yield('/LLE must be a non-negative number', true)
506
+ self[:LLE] = -self[:LLE]
507
+ end
508
+ if key?(:LLO) && self[:LLO] < 0
509
+ yield('/LLO must be a non-negative number', true)
510
+ self[:LLO] = -self[:LLO]
511
+ end
512
+ if self[:LLE] > 0 && self[:LL] == 0
513
+ yield("/LL required to be non-zero if /LLE is set")
514
+ end
515
+ end
516
+
517
+ end
518
+
519
+ end
520
+ end
521
+ end
@@ -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,102 +124,6 @@ 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
127
  # Describes the marker style of a check box or radio button widget.
222
128
  class MarkerStyle
223
129
 
@@ -48,6 +48,9 @@ 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')
51
54
 
52
55
  end
53
56
 
@@ -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.2.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)