hexapdf 1.1.0 → 1.2.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 (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +41 -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 +30 -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.0'
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)