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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +41 -0
- data/lib/hexapdf/cli/command.rb +63 -63
- data/lib/hexapdf/cli/inspect.rb +1 -1
- data/lib/hexapdf/cli/modify.rb +0 -1
- data/lib/hexapdf/cli/optimize.rb +5 -5
- data/lib/hexapdf/configuration.rb +21 -0
- data/lib/hexapdf/content/graphics_state.rb +1 -1
- data/lib/hexapdf/digital_signature/signing/signed_data_creator.rb +1 -1
- data/lib/hexapdf/document/annotations.rb +115 -0
- data/lib/hexapdf/document.rb +30 -7
- data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
- data/lib/hexapdf/font/type1_wrapper.rb +1 -0
- data/lib/hexapdf/type/acro_form/java_script_actions.rb +9 -2
- data/lib/hexapdf/type/acro_form/text_field.rb +9 -2
- data/lib/hexapdf/type/annotation.rb +59 -1
- data/lib/hexapdf/type/annotations/appearance_generator.rb +273 -0
- data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
- data/lib/hexapdf/type/annotations/line.rb +521 -0
- data/lib/hexapdf/type/annotations/widget.rb +2 -96
- data/lib/hexapdf/type/annotations.rb +3 -0
- data/lib/hexapdf/type/form.rb +2 -2
- data/lib/hexapdf/version.rb +1 -1
- data/lib/hexapdf/writer.rb +0 -1
- data/lib/hexapdf/xref_section.rb +7 -4
- data/test/hexapdf/content/test_graphics_state.rb +2 -3
- data/test/hexapdf/content/test_operator.rb +4 -5
- data/test/hexapdf/digital_signature/test_cms_handler.rb +7 -8
- data/test/hexapdf/digital_signature/test_handler.rb +2 -3
- data/test/hexapdf/digital_signature/test_pkcs1_handler.rb +1 -2
- data/test/hexapdf/document/test_annotations.rb +33 -0
- data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
- data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
- data/test/hexapdf/task/test_optimize.rb +1 -1
- data/test/hexapdf/test_document.rb +11 -3
- data/test/hexapdf/test_stream.rb +1 -2
- data/test/hexapdf/test_xref_section.rb +1 -1
- data/test/hexapdf/type/acro_form/test_java_script_actions.rb +21 -0
- data/test/hexapdf/type/acro_form/test_text_field.rb +7 -1
- data/test/hexapdf/type/annotations/test_appearance_generator.rb +398 -0
- data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
- data/test/hexapdf/type/annotations/test_line.rb +189 -0
- data/test/hexapdf/type/annotations/test_widget.rb +0 -81
- data/test/hexapdf/type/test_annotation.rb +55 -0
- data/test/hexapdf/type/test_form.rb +6 -0
- 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: '
|
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
|