hexapdf 1.1.1 → 1.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -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 +28 -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
|