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
@@ -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