hexapdf 1.2.0 → 1.4.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +90 -0
  3. data/README.md +1 -1
  4. data/lib/hexapdf/cli/form.rb +9 -4
  5. data/lib/hexapdf/cli/inspect.rb +13 -4
  6. data/lib/hexapdf/composer.rb +14 -0
  7. data/lib/hexapdf/configuration.rb +15 -0
  8. data/lib/hexapdf/dictionary_fields.rb +1 -1
  9. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -2
  10. data/lib/hexapdf/document/annotations.rb +107 -2
  11. data/lib/hexapdf/document/layout.rb +94 -15
  12. data/lib/hexapdf/document/metadata.rb +10 -3
  13. data/lib/hexapdf/document.rb +9 -0
  14. data/lib/hexapdf/encryption/standard_security_handler.rb +7 -2
  15. data/lib/hexapdf/error.rb +11 -3
  16. data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
  17. data/lib/hexapdf/layout/box.rb +5 -0
  18. data/lib/hexapdf/layout/container_box.rb +63 -28
  19. data/lib/hexapdf/layout/style.rb +129 -20
  20. data/lib/hexapdf/layout/table_box.rb +20 -2
  21. data/lib/hexapdf/object.rb +2 -2
  22. data/lib/hexapdf/pdf_array.rb +25 -3
  23. data/lib/hexapdf/tokenizer.rb +4 -1
  24. data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
  25. data/lib/hexapdf/type/acro_form/field.rb +1 -0
  26. data/lib/hexapdf/type/acro_form/form.rb +7 -6
  27. data/lib/hexapdf/type/annotation.rb +12 -0
  28. data/lib/hexapdf/type/annotations/appearance_generator.rb +169 -16
  29. data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
  30. data/lib/hexapdf/type/annotations/circle.rb +65 -0
  31. data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
  32. data/lib/hexapdf/type/annotations/line.rb +5 -192
  33. data/lib/hexapdf/type/annotations/line_ending_styling.rb +208 -0
  34. data/lib/hexapdf/type/annotations/markup_annotation.rb +0 -1
  35. data/lib/hexapdf/type/annotations/polygon.rb +64 -0
  36. data/lib/hexapdf/type/annotations/polygon_polyline.rb +109 -0
  37. data/lib/hexapdf/type/annotations/polyline.rb +64 -0
  38. data/lib/hexapdf/type/annotations/square.rb +65 -0
  39. data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
  40. data/lib/hexapdf/type/annotations/widget.rb +50 -20
  41. data/lib/hexapdf/type/annotations.rb +9 -0
  42. data/lib/hexapdf/type/measure.rb +57 -0
  43. data/lib/hexapdf/type.rb +1 -0
  44. data/lib/hexapdf/version.rb +1 -1
  45. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +0 -1
  46. data/test/hexapdf/document/test_annotations.rb +42 -0
  47. data/test/hexapdf/document/test_layout.rb +38 -10
  48. data/test/hexapdf/document/test_metadata.rb +13 -1
  49. data/test/hexapdf/encryption/test_standard_security_handler.rb +2 -1
  50. data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
  51. data/test/hexapdf/layout/test_box.rb +8 -0
  52. data/test/hexapdf/layout/test_container_box.rb +34 -6
  53. data/test/hexapdf/layout/test_page_style.rb +1 -1
  54. data/test/hexapdf/layout/test_style.rb +46 -2
  55. data/test/hexapdf/layout/test_table_box.rb +14 -1
  56. data/test/hexapdf/test_composer.rb +7 -0
  57. data/test/hexapdf/test_dictionary_fields.rb +1 -0
  58. data/test/hexapdf/test_object.rb +1 -1
  59. data/test/hexapdf/test_pdf_array.rb +36 -3
  60. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +78 -3
  61. data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
  62. data/test/hexapdf/type/acro_form/test_field.rb +5 -0
  63. data/test/hexapdf/type/acro_form/test_form.rb +17 -1
  64. data/test/hexapdf/type/annotations/test_appearance_generator.rb +210 -0
  65. data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
  66. data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
  67. data/test/hexapdf/type/annotations/test_line.rb +0 -45
  68. data/test/hexapdf/type/annotations/test_line_ending_styling.rb +42 -0
  69. data/test/hexapdf/type/annotations/test_polygon_polyline.rb +29 -0
  70. data/test/hexapdf/type/annotations/test_widget.rb +35 -0
  71. metadata +16 -2
@@ -174,11 +174,59 @@ module HexaPDF
174
174
 
175
175
  alias create_radio_button_appearances create_check_box_appearances
176
176
 
177
- # Creates the appropriate appearances for push buttons.
177
+ # Creates the appropriate appearances for push button fields
178
178
  #
179
- # This is currently a dummy implementation raising an error.
179
+ # The following describes how the appearance is built:
180
+ #
181
+ # * The widget's rectangle /Rect must be defined.
182
+ #
183
+ # * If the font size (used for the caption) is zero, a font size of
184
+ # +acro_form.default_font_size+ is used.
185
+ #
186
+ # * The line width, style and color of the rectangle are taken from the widget's border
187
+ # style. See HexaPDF::Type::Annotations::Widget#border_style.
188
+ #
189
+ # * The background color is determined by the widget's background color. See
190
+ # HexaPDF::Type::Annotations::Widget#background_color.
180
191
  def create_push_button_appearances
181
- raise HexaPDF::Error, "Push button appearance generation not yet supported"
192
+ default_resources = @document.acro_form(create: true).default_resources
193
+ border_style = @widget.border_style
194
+ padding = border_style.width
195
+ marker_style = @widget.marker_style
196
+ font = retrieve_font_information(marker_style.font_name, default_resources)
197
+
198
+ @widget[:AS] = :N
199
+ @widget.flag(:print)
200
+ @widget.unflag(:hidden)
201
+ rect = @widget[:Rect]
202
+
203
+ width, height, matrix = perform_rotation(rect.width, rect.height)
204
+
205
+ form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
206
+ # Wrap existing object in Form class in case the PDF writer didn't include the /Subtype
207
+ # key or the type of the object is wrong; we can do this since we know this has to be a
208
+ # Form object
209
+ unless form.type == :XObject && form[:Subtype] == :Form
210
+ form = @document.wrap(form, type: :XObject, subtype: :Form)
211
+ end
212
+ form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
213
+ Matrix: matrix, Resources: HexaPDF::Object.deep_copy(default_resources)})
214
+ form.contents = ''
215
+
216
+ canvas = form.canvas
217
+ apply_background_and_border(border_style, canvas)
218
+
219
+ style = HexaPDF::Layout::Style.new(font: font, font_size: marker_style.size,
220
+ fill_color: marker_style.color)
221
+ if (text = marker_style.style) && text.kind_of?(String)
222
+ items = @document.layout.text_fragments(marker_style.style, style: style)
223
+ layouter = Layout::TextLayouter.new(style)
224
+ layouter.style.text_align(:center).text_valign(:center).line_spacing(:proportional, 1.25)
225
+ result = layouter.fit(items, width - 2 * padding, height - 2 * padding)
226
+ unless result.lines.empty?
227
+ result.draw(canvas, padding, height - padding)
228
+ end
229
+ end
182
230
  end
183
231
 
184
232
  # Creates the appropriate appearances for text fields, combo box fields and list box fields.
@@ -207,7 +255,8 @@ module HexaPDF
207
255
  # Note: Rich text fields are currently not supported!
208
256
  def create_text_appearances
209
257
  default_resources = @document.acro_form.default_resources
210
- font, font_size, font_color = retrieve_font_information(default_resources)
258
+ font_name, font_size, font_color = @field.parse_default_appearance_string(@widget)
259
+ font = retrieve_font_information(font_name, default_resources)
211
260
  style = HexaPDF::Layout::Style.new(font: font, font_size: font_size, fill_color: font_color)
212
261
  border_style = @widget.border_style
213
262
  padding = [1, border_style.width].max
@@ -475,9 +524,9 @@ module HexaPDF
475
524
  end
476
525
  end
477
526
 
478
- # Returns the font wrapper and font size to be used for a variable text field.
479
- def retrieve_font_information(resources)
480
- font_name, font_size, font_color = @field.parse_default_appearance_string(@widget)
527
+ # Returns the font wrapper, font size and font color to be used for variable text fields and
528
+ # push button captions.
529
+ def retrieve_font_information(font_name, resources)
481
530
  font_object = resources.font(font_name) rescue nil
482
531
  font = font_object&.font_wrapper
483
532
  unless font
@@ -493,7 +542,7 @@ module HexaPDF
493
542
  raise(HexaPDF::Error, "Font #{font_name} of the AcroForm's default resources not usable")
494
543
  end
495
544
  end
496
- [font, font_size, font_color]
545
+ font
497
546
  end
498
547
 
499
548
  # Calculates the font size for single line text fields using auto-sizing, based on the font
@@ -392,6 +392,7 @@ module HexaPDF
392
392
  break # Each annotation dictionary may only appear on one page, see PDF2.0 12.5.2
393
393
  end
394
394
  end
395
+ document.revisions.current.update(self)
395
396
  widget
396
397
  end
397
398
 
@@ -568,12 +568,12 @@ module HexaPDF
568
568
  seen = {} # used for combining field
569
569
 
570
570
  validate_array = lambda do |parent, container|
571
- container.reject! do |field|
571
+ container.map! do |field|
572
572
  if !field.kind_of?(HexaPDF::Object) || !field.kind_of?(HexaPDF::Dictionary) || field.null?
573
573
  yield("Invalid object in AcroForm field hierarchy", true)
574
- next true
574
+ next nil
575
575
  end
576
- next false unless field.key?(:T) # Skip widgets
576
+ next field unless field.key?(:T) # Skip widgets
577
577
 
578
578
  field = Field.wrap(document, field)
579
579
  reject = false
@@ -597,14 +597,15 @@ module HexaPDF
597
597
  widget[:Parent] = other_field
598
598
  kids << widget
599
599
  end
600
+ document.delete(field)
600
601
  reject = true
601
602
  elsif !reject
602
603
  seen[name] = field
603
604
  end
604
605
 
605
- validate_array.call(field, field[:Kids]) if field.key?(:Kids)
606
- reject
607
- end
606
+ validate_array.call(field, field[:Kids]) if !field.null? && field.key?(:Kids)
607
+ reject ? nil : field
608
+ end.compact!
608
609
  end
609
610
  validate_array.call(nil, root_fields)
610
611
 
@@ -113,6 +113,18 @@ module HexaPDF
113
113
 
114
114
  end
115
115
 
116
+ # Border effect dictionary used by square, circle and polygon annotation types.
117
+ #
118
+ # See: PDF2.0 s12.5.4
119
+ class BorderEffect < Dictionary
120
+
121
+ define_type :XXBorderEffect
122
+
123
+ define_field :S, type: Symbol, default: :S, allowed_values: [:C, :S]
124
+ define_field :I, type: Numeric, default: 0, allowed_values: [0, 1, 2]
125
+
126
+ end
127
+
116
128
  extend Utils::BitField
117
129
 
118
130
  define_type :Annot
@@ -73,6 +73,10 @@ module HexaPDF
73
73
  def create_appearance
74
74
  case @annot[:Subtype]
75
75
  when :Line then create_line_appearance
76
+ when :Square then create_square_circle_appearance(:square)
77
+ when :Circle then create_square_circle_appearance(:circle)
78
+ when :Polygon then create_polygon_polyline_appearance(:polygon)
79
+ when :PolyLine then create_polygon_polyline_appearance(:polyline)
76
80
  else
77
81
  raise HexaPDF::Error, "Appearance regeneration for #{@annot[:Subtype]} not yet supported"
78
82
  end
@@ -141,18 +145,8 @@ module HexaPDF
141
145
  # the caption when calculating the bounding box.
142
146
  #
143
147
  # The result could still be improved by tailoring to the specific line ending style.
144
- calculate_le_padding = lambda do |le_style|
145
- case le_style
146
- when :square, :circle, :diamond, :slash, :open_arrow, :closed_arrow
147
- 3 * style.width
148
- when :ropen_arrow, :rclosed_arrow
149
- 10 * style.width
150
- else
151
- 0
152
- end
153
- end
154
- dstart = calculate_le_padding.call(line_ending_style.start_style)
155
- dend = calculate_le_padding.call(line_ending_style.end_style)
148
+ dstart = calculate_line_ending_padding(line_ending_style.start_style, style.width)
149
+ dend = calculate_line_ending_padding(line_ending_style.end_style, style.width)
156
150
  if captioned
157
151
  cap_ulx = x0 + cos_angle * cap_x - sin_angle * cap_y
158
152
  cap_uly = y0 + sin_angle * cap_x + cos_angle * cap_y
@@ -224,6 +218,163 @@ module HexaPDF
224
218
  cap_result.draw(canvas, cap_x + 1, cap_y) if captioned
225
219
  end
226
220
 
221
+ # Creates the appropriate appearance for a square or circle annotation depending on the
222
+ # given +type+ (which can either be +:square+ or +:circle+).
223
+ #
224
+ # The cloudy border effect is not supported.
225
+ #
226
+ # See: HexaPDF::Type::Annotations::Square, HexaPDF::Type::Annotations::Circle
227
+ def create_square_circle_appearance(type)
228
+ # Prepare the annotation
229
+ form = (@annot[:AP] ||= {})[:N] ||=
230
+ @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 0, 0]})
231
+ form.contents = ""
232
+ @annot.flag(:print)
233
+ @annot.unflag(:hidden)
234
+
235
+ rect = @annot[:Rect]
236
+ x, y, w, h = rect.left, rect.bottom, rect.width, rect.height
237
+ border_style = @annot.border_style
238
+ interior_color = @annot.interior_color
239
+ opacity = @annot.opacity
240
+
241
+ # Take the differences array into account. If it exists, the boundary of the actual
242
+ # rectangle is the one with the differences applied to /Rect.
243
+ #
244
+ # If the differences array doesn't exist, we assume that the /Rect is the rectangle we
245
+ # want to draw, with the line width split on both side (like with Canvas#rectangle). In
246
+ # this case we need to update /Rect accordingly so that the line width on the outside is
247
+ # correctly shown.
248
+ line_width_adjustment = border_style.width / 2.0
249
+ if (rd = @annot[:RD])
250
+ x += rd[0]
251
+ y += rd[3]
252
+ w -= rd[0] + rd[2]
253
+ h -= rd[1] + rd[3]
254
+ else
255
+ @annot[:RD] = [0, 0, 0, 0]
256
+ x = rect.left -= line_width_adjustment
257
+ y = rect.bottom -= line_width_adjustment
258
+ w = rect.width += line_width_adjustment
259
+ h = rect.height += line_width_adjustment
260
+ end
261
+ x += line_width_adjustment
262
+ y += line_width_adjustment
263
+ w -= 2 * line_width_adjustment
264
+ h -= 2 * line_width_adjustment
265
+
266
+ x -= rect.left
267
+ y -= rect.bottom
268
+ form[:BBox] = [0, 0, rect.width, rect.height]
269
+
270
+ if border_style.color || interior_color
271
+ canvas = form.canvas
272
+ canvas.opacity(**opacity.to_h)
273
+ canvas.stroke_color(border_style.color) if border_style.color
274
+ canvas.fill_color(interior_color) if interior_color
275
+ canvas.line_width(border_style.width)
276
+ canvas.line_dash_pattern(border_style.style) if border_style.style.kind_of?(Array)
277
+
278
+ if type == :square
279
+ canvas.rectangle(x, y, w, h)
280
+ else
281
+ canvas.ellipse(x + w / 2.0, y + h / 2.0, a: w / 2.0, b: h / 2.0)
282
+ end
283
+
284
+ if border_style.color && interior_color
285
+ canvas.fill_stroke
286
+ elsif border_style.color
287
+ canvas.stroke
288
+ else
289
+ canvas.fill
290
+ end
291
+ end
292
+ end
293
+
294
+ # Creates the appropriate appearance for a polygon or polyline annotation depending on the
295
+ # given +type+ (which can either be +:polygon+ or +:polyline+).
296
+ #
297
+ # The cloudy border effect is not supported.
298
+ #
299
+ # See: HexaPDF::Type::Annotations::Polygon, HexaPDF::Type::Annotations::Polyline
300
+ def create_polygon_polyline_appearance(type)
301
+ # Prepare the annotation
302
+ form = (@annot[:AP] ||= {})[:N] ||=
303
+ @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 0, 0]})
304
+ form.contents = ""
305
+ @annot.flag(:print)
306
+ @annot.unflag(:hidden)
307
+
308
+ # Get all needed values from the annotation
309
+ vertices = @annot.vertices
310
+ border_style = @annot.border_style
311
+ line_ending_style = @annot.line_ending_style
312
+ opacity = @annot.opacity
313
+ interior_color = @annot.interior_color
314
+
315
+ # Calculate the annotation's rectangle as well as the form bounding box
316
+ padding_start = calculate_line_ending_padding(line_ending_style.start_style, border_style.width)
317
+ padding_end = calculate_line_ending_padding(line_ending_style.end_style, border_style.width)
318
+ x_coords, y_coords = vertices.partition.with_index {|_, index| index.even? }
319
+ min_x, max_x = (x_coords + [x_coords[0] + padding_start, x_coords[0] - padding_start,
320
+ x_coords[-1] + padding_end, x_coords[-1] - padding_end]).minmax
321
+ min_y, max_y = (y_coords + [y_coords[0] + padding_start, y_coords[0] - padding_start,
322
+ y_coords[-1] + padding_end, y_coords[-1] - padding_end]).minmax
323
+
324
+ padding = 4 * border_style.width
325
+ rect = [min_x - padding, min_y - padding, max_x + padding, max_y + padding]
326
+ @annot[:Rect] = rect
327
+ form[:BBox] = rect.dup
328
+
329
+ return if vertices.length < 4
330
+
331
+ # Set the appropriate graphics state
332
+ canvas = form.canvas(translate: false)
333
+ canvas.opacity(**opacity.to_h)
334
+ canvas.stroke_color(border_style.color) if border_style.color
335
+ canvas.fill_color(interior_color) if interior_color
336
+ canvas.line_width(border_style.width)
337
+ canvas.line_dash_pattern(border_style.style) if border_style.style.kind_of?(Array)
338
+
339
+ stroke_op = (border_style.color ? :stroke : :end_path)
340
+ fill_op = (border_style.color && interior_color ? :fill_stroke :
341
+ (border_style.color ? :stroke : (interior_color ? :fill : :end_path)))
342
+
343
+ # Draw the polygon/polyline
344
+ canvas.send(type, *vertices)
345
+ canvas.send(type == :polygon ? fill_op : stroke_op)
346
+
347
+ return unless type == :polyline
348
+
349
+ # Draw line endings
350
+ angle_start = Math.atan2(y_coords[1] - y_coords[0], x_coords[1] - x_coords[0])
351
+ angle_end = Math.atan2(y_coords[-2] - y_coords[-1], x_coords[-2] - x_coords[-1])
352
+ if line_ending_style.start_style != :none
353
+ do_fill = draw_line_ending(canvas, line_ending_style.start_style,
354
+ x_coords[0], y_coords[0], border_style.width, angle_start)
355
+ canvas.send(do_fill ? fill_op : stroke_op)
356
+ end
357
+ if line_ending_style.end_style != :none
358
+ do_fill = draw_line_ending(canvas, line_ending_style.end_style,
359
+ x_coords[-1], y_coords[-1], border_style.width, angle_end)
360
+ canvas.send(do_fill ? fill_op : stroke_op)
361
+ end
362
+
363
+ end
364
+
365
+ # Calculates the padding needed around the line endings based on the line ending +style+ and
366
+ # the +border_width+.
367
+ def calculate_line_ending_padding(style, border_width)
368
+ case style
369
+ when :square, :circle, :diamond, :slash, :open_arrow, :closed_arrow
370
+ 3 * border_width
371
+ when :ropen_arrow, :rclosed_arrow
372
+ 10 * border_width
373
+ else
374
+ 0
375
+ end
376
+ end
377
+
227
378
  # Draws the line ending style +type+ at the position (+x+, +y+) and returns +true+ if the
228
379
  # shape needs to be filled.
229
380
  #
@@ -244,11 +395,13 @@ module HexaPDF
244
395
  canvas.polygon(x + lw3, y, x, y + lw3, x - lw3, y, x, y - lw3)
245
396
  true
246
397
  when :open_arrow, :closed_arrow, :ropen_arrow, :rclosed_arrow
247
- arrow_cos = Math.cos(Math::PI / 6 + angle)
248
- arrow_sin = Math.sin(Math::PI / 6 + angle)
398
+ arrow_cos_up = Math.cos(angle + Math::PI / 6)
399
+ arrow_sin_up = Math.sin(angle + Math::PI / 6)
400
+ arrow_cos_down = Math.cos(angle - Math::PI / 6)
401
+ arrow_sin_down = Math.sin(angle - Math::PI / 6)
249
402
  dir = (type == :ropen_arrow || type == :rclosed_arrow ? -1 : 1)
250
- canvas.polyline(x + dir * arrow_cos * 3 * lw3, y + arrow_sin * 3 * lw3, x, y,
251
- x + dir * arrow_cos * 3 * lw3, y - arrow_sin * 3 * lw3)
403
+ canvas.polyline(x + dir * arrow_cos_up * 3 * lw3, y + arrow_sin_up * 3 * lw3, x, y,
404
+ x + dir * arrow_cos_down * 3 * lw3, y + arrow_sin_down * 3 * lw3)
252
405
  if type == :closed_arrow || type == :rclosed_arrow
253
406
  canvas.close_subpath
254
407
  true
@@ -0,0 +1,99 @@
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/annotations'
38
+
39
+ module HexaPDF
40
+ module Type
41
+ module Annotations
42
+
43
+ # This module provides a convenience method for getting and setting the border effect for
44
+ # square, circle and polygon annotations.
45
+ #
46
+ # See: PDF2.0 s12.5.4
47
+ module BorderEffect
48
+
49
+ # :call-seq:
50
+ # annot.border_effect => border_effect
51
+ # annot.border_effect(type) => annot
52
+ #
53
+ # Returns the border effect of the annotation when no argument is given. Otherwise sets the
54
+ # border effect of the annotation and returns self.
55
+ #
56
+ # The argument type can have the following values:
57
+ #
58
+ # +:none+:: No border effect is used.
59
+ #
60
+ # +:cloudy+:: The border appears "cloudy" (as a series of convex curved line segments).
61
+ #
62
+ # +:cloudier+:: Like +:cloudy+ but more intense.
63
+ #
64
+ # +:cloudiest+:: Like +:cloudier+ but still more intense.
65
+ def border_effect(type = :UNSET)
66
+ if type == :UNSET
67
+ be = self[:BE]
68
+ if !be || be[:S] != :C
69
+ :none
70
+ else
71
+ case be[:I]
72
+ when 0 then :cloudy
73
+ when 1 then :cloudier
74
+ when 2 then :cloudiest
75
+ else :cloudy
76
+ end
77
+ end
78
+ else
79
+ case type
80
+ when nil, :none
81
+ delete(:BE)
82
+ when :cloudy
83
+ self[:BE] = {S: :C, I: 0}
84
+ when :cloudier
85
+ self[:BE] = {S: :C, I: 1}
86
+ when :cloudiest
87
+ self[:BE] = {S: :C, I: 2}
88
+ else
89
+ raise ArgumentError, "Unknown value #{type} for type argument"
90
+ end
91
+ self
92
+ end
93
+ end
94
+
95
+ end
96
+
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,65 @@
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/annotations'
38
+
39
+ module HexaPDF
40
+ module Type
41
+ module Annotations
42
+
43
+ # A circle annotation displays an ellipse inside the annotation rectangle (the "circle" name
44
+ # defined by the PDF specification is a bit misleading).
45
+ #
46
+ # Also see SquareCircle for more information.
47
+ #
48
+ # Example:
49
+ #
50
+ # #>pdf-small
51
+ # doc.annotations.create_ellipse(doc.pages[0], 50, 50, a: 30, b: 20).
52
+ # border_style(color: "hp-blue", width: 2, style: [3, 1]).
53
+ # interior_color("hp-orange").
54
+ # regenerate_appearance
55
+ #
56
+ # See: PDF2.0 s12.5.6.8, HexaPDF::Type::Annotations::SquareCircle,
57
+ class Circle < SquareCircle
58
+
59
+ define_field :Subtype, type: Symbol, required: true, default: :Circle
60
+
61
+ end
62
+
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,84 @@
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/annotations'
38
+
39
+ module HexaPDF
40
+ module Type
41
+ module Annotations
42
+
43
+ # This module provides a convenience method for getting and setting the interior color for
44
+ # various annotations.
45
+ #
46
+ # See: PDF2.0 s12.5
47
+ module InteriorColor
48
+
49
+ # :call-seq:
50
+ # line.interior_color => color or nil
51
+ # line.interior_color(*color) => line
52
+ #
53
+ # Returns the interior color or +nil+ (in case the interior color should be transparent)
54
+ # when no argument is given. Otherwise sets the interior color and returns self.
55
+ #
56
+ # How the interior color is used depends on the concrete annotation type. For line
57
+ # annotations, for example, it is the color to fill the line endings
58
+ #
59
+ # +color+:: The interior color. See
60
+ # HexaPDF::Content::ColorSpace.device_color_from_specification for information on
61
+ # the allowed arguments.
62
+ #
63
+ # If the special value +:transparent+ is used when setting the color, no color is
64
+ # used for filling.
65
+ def interior_color(*color)
66
+ if color.empty?
67
+ color = self[:IC]
68
+ color && !color.empty? ? Content::ColorSpace.prenormalized_device_color(color.value) : nil
69
+ else
70
+ color = if color.length == 1 && color.first == :transparent
71
+ []
72
+ else
73
+ Content::ColorSpace.device_color_from_specification(color).components
74
+ end
75
+ self[:IC] = color
76
+ self
77
+ end
78
+ end
79
+
80
+ end
81
+
82
+ end
83
+ end
84
+ end