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
@@ -133,7 +133,7 @@ module HexaPDF
133
133
  define_field :OC, type: Dictionary, version: '1.5'
134
134
  define_field :AF, type: PDFArray, version: '2.0'
135
135
  define_field :ca, type: Numeric, default: 1.0, version: '2.0'
136
- define_field :CA, type: Numeric, default: 1.0, version: '2.0'
136
+ define_field :CA, type: Numeric, default: 1.0, version: '1.4'
137
137
  define_field :BM, type: Symbol, version: '2.0'
138
138
  define_field :Lang, type: String, version: '2.0'
139
139
 
@@ -259,6 +259,64 @@ module HexaPDF
259
259
  xobject
260
260
  end
261
261
 
262
+ # Regenerates the appearance stream of the annotation.
263
+ #
264
+ # This uses the information stored in the annotation to regenerate the appearance.
265
+ #
266
+ # See: Annotations::AppearanceGenerator
267
+ def regenerate_appearance
268
+ appearance_generator_class = document.config.constantize('annotation.appearance_generator')
269
+ appearance_generator_class.new(self).create_appearance
270
+ end
271
+
272
+ # :call-seq:
273
+ # annot.contents => contents or +nil+
274
+ # annot.contents(text) => annot
275
+ #
276
+ # Returns the text of the annotation when no argument is given. Otherwise sets the text and
277
+ # returns self.
278
+ #
279
+ # The contents is used differently depending on the annotation type. It is either the text
280
+ # that should be displayed for the annotation or an alternate description of the annotation's
281
+ # contents.
282
+ #
283
+ # A value of +nil+ means deleting the existing contents entry.
284
+ def contents(text = :UNSET)
285
+ if text == :UNSET
286
+ self[:Contents]
287
+ else
288
+ self[:Contents] = text
289
+ self
290
+ end
291
+ end
292
+
293
+ # Describes the opacity values +fill_alpha+ and +stroke_alpha+ of an annotation.
294
+ #
295
+ # See Annotation#opacity
296
+ Opacity = Struct.new(:fill_alpha, :stroke_alpha)
297
+
298
+ # :call-seq:
299
+ # annotation.opacity => current_values
300
+ # annotation.opacity(fill_alpha:) => annotation
301
+ # annotation.opacity(stroke_alpha:) => annotation
302
+ # annotation.opacity(fill_alpha:, stroke_alpha:) => annotation
303
+ #
304
+ # Returns an Opacity instance representing the fill and stroke alpha values when no arguments
305
+ # are given. Otherwise sets the provided alpha values and returns self.
306
+ #
307
+ # The fill and stroke alpha values are used when regenerating the annotation's appearance
308
+ # stream and determine how opaque drawn elements will be. Note that the fill alpha value
309
+ # applies not just to fill values but to all non-stroking operations (e.g. images, ...).
310
+ def opacity(fill_alpha: nil, stroke_alpha: nil)
311
+ if !fill_alpha.nil? || !stroke_alpha.nil?
312
+ self[:CA] = stroke_alpha unless stroke_alpha.nil?
313
+ self[:ca] = fill_alpha unless fill_alpha.nil?
314
+ self
315
+ else
316
+ Opacity.new(key?(:ca) ? self[:ca] : self[:CA], self[:CA])
317
+ end
318
+ end
319
+
262
320
  private
263
321
 
264
322
  def perform_validation(&block) #:nodoc:
@@ -0,0 +1,273 @@
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/error'
38
+
39
+ module HexaPDF
40
+ module Type
41
+ module Annotations
42
+
43
+ # The AppearanceGenerator class provides methods for generating the appearance streams of
44
+ # annotations except those for widgets (see HexaPDF::Type::AcroForm::AppearanceGenerator for
45
+ # those).
46
+ #
47
+ # There is one private create_TYPE_appearance method for each annotation type. This allows
48
+ # subclassing the appearance generator and adjusting the appearances to one's needs.
49
+ #
50
+ # By default, an existing appearance is overwritten and the +:print+ flag is set as well as
51
+ # the +:hidden+ flag unset on the annotation so that the appearance will appear on print-outs.
52
+ #
53
+ # Also note that the annotation's /Rect entry is modified so that it contains the whole
54
+ # generated appearance.
55
+ #
56
+ # The visual appearances are chosen to be similar to those used by Adobe Acrobat and others.
57
+ # By subclassing and overriding the necessary methods it is possible to define custom
58
+ # appearances.
59
+ #
60
+ # The default annotation appearance generator for a document can be changed using the
61
+ # 'annotation.appearance_generator' configuration option.
62
+ #
63
+ # See: PDF2.0 s12.5
64
+ class AppearanceGenerator
65
+
66
+ # Creates a new instance for the given +annotation+.
67
+ def initialize(annotation)
68
+ @annot = annotation
69
+ @document = annotation.document
70
+ end
71
+
72
+ # Creates the appropriate appearance for the annotation provided on initialization.
73
+ def create_appearance
74
+ case @annot[:Subtype]
75
+ when :Line then create_line_appearance
76
+ else
77
+ raise HexaPDF::Error, "Appearance regeneration for #{@annot[:Subtype]} not yet supported"
78
+ end
79
+ end
80
+
81
+ private
82
+
83
+ # Creates the appropriate appearance for a line annotation.
84
+ #
85
+ # Nearly all the needed information can be taken from the annotation object itself. However,
86
+ # the PDF specification doesn't specify where to take the font related information (font,
87
+ # size, alignment...) from. Therefore this is currently hard-coded as left-aligned Helvetica
88
+ # in size 9.
89
+ #
90
+ # There are also some other decisions that are left to the implementation, like padding
91
+ # around the annotation or the size of the line ending shapes. Those are implemented to be
92
+ # similar to how viewers create the appearance.
93
+ #
94
+ # See: HexaPDF::Type::Annotations::Line
95
+ def create_line_appearance
96
+ # Prepare the annotation
97
+ form = (@annot[:AP] ||= {})[:N] ||=
98
+ @document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 0, 0]})
99
+ form.contents = ""
100
+ @annot.flag(:print)
101
+ @annot.unflag(:hidden)
102
+
103
+ # Get or calculate all needed values from the annotation
104
+ x0, y0, x1, y1 = @annot.line
105
+ style = @annot.border_style
106
+ line_ending_style = @annot.line_ending_style
107
+ opacity = @annot.opacity
108
+ ll = @annot.leader_line_length
109
+ lle = @annot.leader_line_extension_length
110
+ llo = @annot.leader_line_offset
111
+
112
+ angle = Math.atan2(y1 - y0, x1 - x0)
113
+ cos_angle = Math.cos(angle)
114
+ sin_angle = Math.sin(angle)
115
+ line_length = Math.sqrt((y1 - y0) ** 2 + (x1 - x0) ** 2)
116
+ ll_sign = (ll > 0 ? 1 : -1)
117
+ ll_y = ll_sign * (ll.abs + lle + llo)
118
+ line_y = (ll != 0 ? ll_sign * (llo + ll.abs) : 0)
119
+
120
+ captioned = @annot.captioned
121
+ contents = @annot.contents.to_s
122
+ if captioned && !contents.empty?
123
+ cap_position = @annot.caption_position
124
+ cap_style = HexaPDF::Layout::Style.new(font: 'Helvetica', font_size: 9,
125
+ fill_color: style.color || 'black',
126
+ line_spacing: 1.25)
127
+ cap_items = @document.layout.text_fragments(contents, style: cap_style)
128
+ layouter = Layout::TextLayouter.new(cap_style)
129
+ cap_result = layouter.fit(cap_items, 2**20, 2**20)
130
+ cap_width = cap_result.lines.max_by(&:width).width + 2 # for padding left/right
131
+ cap_offset = @annot.caption_offset
132
+
133
+ cap_x = (line_length - cap_width) / 2.0 + cap_offset[0]
134
+ # Note that the '+ 2' is just so that there is a small gap to the line
135
+ cap_y = line_y + cap_offset[1] +
136
+ (cap_position == :inline ? cap_result.height / 2.0 : cap_result.height + 2)
137
+ end
138
+
139
+ # Calculate annotation rectangle and form bounding box. This considers the line's start
140
+ # and end points as well as the end points of the leader lines, the line ending style and
141
+ # the caption when calculating the bounding box.
142
+ #
143
+ # 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)
156
+ if captioned
157
+ cap_ulx = x0 + cos_angle * cap_x - sin_angle * cap_y
158
+ cap_uly = y0 + sin_angle * cap_x + cos_angle * cap_y
159
+ end
160
+ min_x, max_x = [x0, x0 - sin_angle * ll_y, x0 - sin_angle * line_y - cos_angle * dstart,
161
+ x1, x1 - sin_angle * ll_y, x1 - sin_angle * line_y + cos_angle * dend,
162
+ *([cap_ulx,
163
+ cap_ulx + cos_angle * cap_width,
164
+ cap_ulx - sin_angle * cap_result.height,
165
+ cap_ulx + cos_angle * cap_width - sin_angle * cap_result.height
166
+ ] if captioned)
167
+ ].minmax
168
+ min_y, max_y = [y0, y0 + cos_angle * ll_y,
169
+ y0 + cos_angle * line_y - ([cos_angle, sin_angle].max) * dstart,
170
+ y1, y1 + cos_angle * ll_y,
171
+ y1 + cos_angle * line_y + ([cos_angle, sin_angle].max) * dend,
172
+ *([cap_uly,
173
+ cap_uly + sin_angle * cap_width,
174
+ cap_uly - cos_angle * cap_result.height,
175
+ cap_uly + sin_angle * cap_width - cos_angle * cap_result.height
176
+ ] if captioned)
177
+ ].minmax
178
+
179
+ padding = 4 * style.width
180
+ rect = [min_x - padding, min_y - padding, max_x + padding, max_y + padding]
181
+ @annot[:Rect] = rect
182
+ form[:BBox] = rect.dup
183
+
184
+ # Set the appropriate graphics state and transform the canvas so that the line is
185
+ # unrotated and its start point at the origin.
186
+ canvas = form.canvas(translate: false)
187
+ canvas.opacity(**opacity.to_h)
188
+ canvas.stroke_color(style.color) if style.color
189
+ canvas.fill_color(@annot.interior_color) if @annot.interior_color
190
+ canvas.line_width(style.width)
191
+ canvas.line_dash_pattern(style.style) if style.style.kind_of?(Array)
192
+ canvas.transform(cos_angle, sin_angle, -sin_angle, cos_angle, x0, y0)
193
+
194
+ stroke_op = (style.color ? :stroke : :end_path)
195
+ fill_op = (style.color && @annot.interior_color ? :fill_stroke :
196
+ (style.color ? :stroke : :fill))
197
+
198
+ # Draw leader lines and line
199
+ if ll != 0
200
+ canvas.line(0, ll_sign * llo, 0, ll_y)
201
+ canvas.line(line_length, ll_sign * llo, line_length, ll_y)
202
+ end
203
+ if captioned && cap_position == :inline
204
+ canvas.line(0, line_y, [[0, cap_x].max, line_length].min, line_y)
205
+ canvas.line([[cap_x + cap_width, 0].max, line_length].min, line_y, line_length, line_y)
206
+ else
207
+ canvas.line(0, line_y, line_length, line_y)
208
+ end
209
+ canvas.send(stroke_op)
210
+
211
+ # Draw line endings
212
+ if line_ending_style.start_style != :none
213
+ do_fill = draw_line_ending(canvas, line_ending_style.start_style, 0, line_y,
214
+ style.width, 0)
215
+ canvas.send(do_fill ? fill_op : stroke_op)
216
+ end
217
+ if line_ending_style.end_style != :none
218
+ do_fill = draw_line_ending(canvas, line_ending_style.end_style, line_length, line_y,
219
+ style.width, Math::PI)
220
+ canvas.send(do_fill ? fill_op : stroke_op)
221
+ end
222
+
223
+ # Draw caption, adding half of the padding added to cap_width
224
+ cap_result.draw(canvas, cap_x + 1, cap_y) if captioned
225
+ end
226
+
227
+ # Draws the line ending style +type+ at the position (+x+, +y+) and returns +true+ if the
228
+ # shape needs to be filled.
229
+ #
230
+ # The argument +angle+ specifies the angle at which the line ending style should be drawn.
231
+ #
232
+ # The +line_width+ is needed because the size of the line ending depends on it.
233
+ def draw_line_ending(canvas, type, x, y, line_width, angle)
234
+ lw3 = 3 * line_width
235
+
236
+ case type
237
+ when :square
238
+ canvas.rectangle(x - lw3, y - lw3, 2 * lw3, 2 * lw3)
239
+ true
240
+ when :circle
241
+ canvas.circle(x, y, lw3)
242
+ true
243
+ when :diamond
244
+ canvas.polygon(x + lw3, y, x, y + lw3, x - lw3, y, x, y - lw3)
245
+ true
246
+ 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)
249
+ 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)
252
+ if type == :closed_arrow || type == :rclosed_arrow
253
+ canvas.close_subpath
254
+ true
255
+ else
256
+ false
257
+ end
258
+ when :butt
259
+ canvas.line(x, y + lw3, x, y - lw3)
260
+ false
261
+ when :slash
262
+ sin_60 = Math.sin(Math::PI / 3)
263
+ cos_60 = Math.cos(Math::PI / 3)
264
+ canvas.line(x + cos_60 * lw3, y + sin_60 * lw3, x - cos_60 * lw3, y - sin_60 * lw3)
265
+ false
266
+ end
267
+ end
268
+
269
+ end
270
+
271
+ end
272
+ end
273
+ end
@@ -0,0 +1,160 @@
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
+ require 'hexapdf/content'
39
+ require 'hexapdf/serializer'
40
+
41
+ module HexaPDF
42
+ module Type
43
+ module Annotations
44
+
45
+ # This module provides a convenience method for getting and setting the border style and is
46
+ # included in the annotations that need it.
47
+ #
48
+ # See: PDF2.0 s12.5.4
49
+ module BorderStyling
50
+
51
+ # Describes the border of an annotation.
52
+ #
53
+ # The +color+ property is either +nil+ if the border is transparent or else a device color
54
+ # object - see HexaPDF::Content::ColorSpace.
55
+ #
56
+ # The +style+ property can be one of the following:
57
+ #
58
+ # :solid:: Solid line.
59
+ # :beveled:: Embossed rectangle seemingly raised above the surface of the page.
60
+ # :inset:: Engraved rectangle receeding into the page.
61
+ # :underlined:: Underlined, i.e. only the bottom border is draw.
62
+ # Array: Dash array describing how to dash the line.
63
+ BorderStyle = Struct.new(:width, :color, :style, :horizontal_corner_radius,
64
+ :vertical_corner_radius)
65
+
66
+ # :call-seq:
67
+ # annot.border_style => border_style
68
+ # annot.border_style(color: 0, width: 1, style: :solid) => annot
69
+ #
70
+ # Returns a BorderStyle instance representing the border style of the annotation when no
71
+ # argument is given. Otherwise sets the border style of the annotation and returns self.
72
+ #
73
+ # When setting a border style, arguments that are not provided will use the default: a
74
+ # border with a solid, black, 1pt wide line. This also means that multiple invocations will
75
+ # reset *all* prior values.
76
+ #
77
+ # +color+:: The color of the border. See
78
+ # HexaPDF::Content::ColorSpace.device_color_from_specification for information on
79
+ # the allowed arguments.
80
+ #
81
+ # If the special value +:transparent+ is used when setting the color, a
82
+ # transparent is used. A transparent border will return a +nil+ value when getting
83
+ # the border color.
84
+ #
85
+ # +width+:: The width of the border. If set to 0, no border is shown.
86
+ #
87
+ # +style+:: Defines how the border is drawn. can be one of the following:
88
+ #
89
+ # +:solid+:: Draws a solid border.
90
+ # +:beveled+:: Draws a beveled border.
91
+ # +:inset+:: Draws an inset border.
92
+ # +:underlined+:: Draws only the bottom border.
93
+ # Array:: An array specifying a line dash pattern (see
94
+ # HexaPDF::Content::LineDashPattern)
95
+ def border_style(color: nil, width: nil, style: nil)
96
+ if color || width || style
97
+ color = if color == :transparent
98
+ []
99
+ else
100
+ Content::ColorSpace.device_color_from_specification(color || 0).components
101
+ end
102
+ width ||= 1
103
+ style ||= :solid
104
+
105
+ if self[:Subtype] == :Widget
106
+ (self[:MK] ||= {})[:BC] = color
107
+ else
108
+ self[:C] = color
109
+ end
110
+ bs = self[:BS] = {W: width}
111
+ case style
112
+ when :solid then bs[:S] = :S
113
+ when :beveled then bs[:S] = :B
114
+ when :inset then bs[:S] = :I
115
+ when :underlined then bs[:S] = :U
116
+ when Array
117
+ bs[:S] = :D
118
+ bs[:D] = style
119
+ else
120
+ raise ArgumentError, "Unknown value #{style} for style argument"
121
+ end
122
+ self
123
+ else
124
+ result = BorderStyle.new(1, nil, :solid, 0, 0)
125
+ bc = if self[:Subtype] == :Widget
126
+ (ac = self[:MK]) && (bc = ac[:BC])
127
+ else
128
+ self[:C]
129
+ end
130
+ if bc && !bc.empty?
131
+ result.color = Content::ColorSpace.prenormalized_device_color(bc.value)
132
+ end
133
+
134
+ if (bs = self[:BS])
135
+ result.width = bs[:W] if bs.key?(:W)
136
+ result.style = case bs[:S]
137
+ when :S then :solid
138
+ when :B then :beveled
139
+ when :I then :inset
140
+ when :U then :underlined
141
+ when :D then bs[:D].value
142
+ else :solid
143
+ end
144
+ elsif key?(:Border)
145
+ border = self[:Border]
146
+ result.horizontal_corner_radius = border[0]
147
+ result.vertical_corner_radius = border[1]
148
+ result.width = border[2]
149
+ result.style = border[3] if border[3]
150
+ end
151
+
152
+ result
153
+ end
154
+ end
155
+
156
+ end
157
+
158
+ end
159
+ end
160
+ end