hexapdf 1.1.1 → 1.3.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 +79 -0
- data/README.md +1 -1
- data/lib/hexapdf/cli/command.rb +63 -63
- data/lib/hexapdf/cli/inspect.rb +14 -5
- data/lib/hexapdf/cli/modify.rb +0 -1
- data/lib/hexapdf/cli/optimize.rb +5 -5
- data/lib/hexapdf/composer.rb +14 -0
- data/lib/hexapdf/configuration.rb +26 -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 +173 -0
- data/lib/hexapdf/document/layout.rb +45 -6
- data/lib/hexapdf/document.rb +28 -7
- data/lib/hexapdf/error.rb +11 -3
- data/lib/hexapdf/font/true_type/subsetter.rb +15 -2
- data/lib/hexapdf/font/true_type_wrapper.rb +1 -0
- data/lib/hexapdf/font/type1_wrapper.rb +1 -0
- data/lib/hexapdf/layout/style.rb +101 -7
- data/lib/hexapdf/object.rb +2 -2
- data/lib/hexapdf/pdf_array.rb +25 -3
- data/lib/hexapdf/tokenizer.rb +4 -1
- data/lib/hexapdf/type/acro_form/appearance_generator.rb +57 -8
- data/lib/hexapdf/type/acro_form/field.rb +1 -0
- data/lib/hexapdf/type/acro_form/form.rb +7 -6
- 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 +71 -1
- data/lib/hexapdf/type/annotations/appearance_generator.rb +348 -0
- data/lib/hexapdf/type/annotations/border_effect.rb +99 -0
- data/lib/hexapdf/type/annotations/border_styling.rb +160 -0
- data/lib/hexapdf/type/annotations/circle.rb +65 -0
- data/lib/hexapdf/type/annotations/interior_color.rb +84 -0
- data/lib/hexapdf/type/annotations/line.rb +490 -0
- data/lib/hexapdf/type/annotations/square.rb +65 -0
- data/lib/hexapdf/type/annotations/square_circle.rb +77 -0
- data/lib/hexapdf/type/annotations/widget.rb +52 -116
- data/lib/hexapdf/type/annotations.rb +8 -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 +55 -0
- data/test/hexapdf/document/test_layout.rb +24 -2
- data/test/hexapdf/font/test_true_type_wrapper.rb +7 -0
- data/test/hexapdf/font/test_type1_wrapper.rb +7 -0
- data/test/hexapdf/font/true_type/test_subsetter.rb +10 -0
- data/test/hexapdf/layout/test_style.rb +27 -2
- data/test/hexapdf/task/test_optimize.rb +1 -1
- data/test/hexapdf/test_composer.rb +7 -0
- data/test/hexapdf/test_document.rb +11 -3
- data/test/hexapdf/test_object.rb +1 -1
- data/test/hexapdf/test_pdf_array.rb +36 -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_appearance_generator.rb +78 -3
- data/test/hexapdf/type/acro_form/test_button_field.rb +7 -6
- data/test/hexapdf/type/acro_form/test_field.rb +5 -0
- data/test/hexapdf/type/acro_form/test_form.rb +17 -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 +482 -0
- data/test/hexapdf/type/annotations/test_border_effect.rb +59 -0
- data/test/hexapdf/type/annotations/test_border_styling.rb +114 -0
- data/test/hexapdf/type/annotations/test_interior_color.rb +37 -0
- data/test/hexapdf/type/annotations/test_line.rb +169 -0
- data/test/hexapdf/type/annotations/test_widget.rb +35 -81
- data/test/hexapdf/type/test_annotation.rb +55 -0
- data/test/hexapdf/type/test_form.rb +6 -0
- metadata +17 -2
@@ -174,11 +174,59 @@ module HexaPDF
|
|
174
174
|
|
175
175
|
alias create_radio_button_appearances create_check_box_appearances
|
176
176
|
|
177
|
-
# Creates the appropriate appearances for push
|
177
|
+
# Creates the appropriate appearances for push button fields
|
178
178
|
#
|
179
|
-
#
|
179
|
+
# The following describes how the appearance is built:
|
180
|
+
#
|
181
|
+
# * The widget's rectangle /Rect must be defined.
|
182
|
+
#
|
183
|
+
# * If the font size (used for the caption) is zero, a font size of
|
184
|
+
# +acro_form.default_font_size+ is used.
|
185
|
+
#
|
186
|
+
# * The line width, style and color of the rectangle are taken from the widget's border
|
187
|
+
# style. See HexaPDF::Type::Annotations::Widget#border_style.
|
188
|
+
#
|
189
|
+
# * The background color is determined by the widget's background color. See
|
190
|
+
# HexaPDF::Type::Annotations::Widget#background_color.
|
180
191
|
def create_push_button_appearances
|
181
|
-
|
192
|
+
default_resources = @document.acro_form(create: true).default_resources
|
193
|
+
border_style = @widget.border_style
|
194
|
+
padding = border_style.width
|
195
|
+
marker_style = @widget.marker_style
|
196
|
+
font = retrieve_font_information(marker_style.font_name, default_resources)
|
197
|
+
|
198
|
+
@widget[:AS] = :N
|
199
|
+
@widget.flag(:print)
|
200
|
+
@widget.unflag(:hidden)
|
201
|
+
rect = @widget[:Rect]
|
202
|
+
|
203
|
+
width, height, matrix = perform_rotation(rect.width, rect.height)
|
204
|
+
|
205
|
+
form = (@widget[:AP] ||= {})[:N] ||= @document.add({Type: :XObject, Subtype: :Form})
|
206
|
+
# Wrap existing object in Form class in case the PDF writer didn't include the /Subtype
|
207
|
+
# key or the type of the object is wrong; we can do this since we know this has to be a
|
208
|
+
# Form object
|
209
|
+
unless form.type == :XObject && form[:Subtype] == :Form
|
210
|
+
form = @document.wrap(form, type: :XObject, subtype: :Form)
|
211
|
+
end
|
212
|
+
form.value.replace({Type: :XObject, Subtype: :Form, BBox: [0, 0, width, height],
|
213
|
+
Matrix: matrix, Resources: HexaPDF::Object.deep_copy(default_resources)})
|
214
|
+
form.contents = ''
|
215
|
+
|
216
|
+
canvas = form.canvas
|
217
|
+
apply_background_and_border(border_style, canvas)
|
218
|
+
|
219
|
+
style = HexaPDF::Layout::Style.new(font: font, font_size: marker_style.size,
|
220
|
+
fill_color: marker_style.color)
|
221
|
+
if (text = marker_style.style) && text.kind_of?(String)
|
222
|
+
items = @document.layout.text_fragments(marker_style.style, style: style)
|
223
|
+
layouter = Layout::TextLayouter.new(style)
|
224
|
+
layouter.style.text_align(:center).text_valign(:center).line_spacing(:proportional, 1.25)
|
225
|
+
result = layouter.fit(items, width - 2 * padding, height - 2 * padding)
|
226
|
+
unless result.lines.empty?
|
227
|
+
result.draw(canvas, padding, height - padding)
|
228
|
+
end
|
229
|
+
end
|
182
230
|
end
|
183
231
|
|
184
232
|
# Creates the appropriate appearances for text fields, combo box fields and list box fields.
|
@@ -207,7 +255,8 @@ module HexaPDF
|
|
207
255
|
# Note: Rich text fields are currently not supported!
|
208
256
|
def create_text_appearances
|
209
257
|
default_resources = @document.acro_form.default_resources
|
210
|
-
|
258
|
+
font_name, font_size, font_color = @field.parse_default_appearance_string(@widget)
|
259
|
+
font = retrieve_font_information(font_name, default_resources)
|
211
260
|
style = HexaPDF::Layout::Style.new(font: font, font_size: font_size, fill_color: font_color)
|
212
261
|
border_style = @widget.border_style
|
213
262
|
padding = [1, border_style.width].max
|
@@ -475,9 +524,9 @@ module HexaPDF
|
|
475
524
|
end
|
476
525
|
end
|
477
526
|
|
478
|
-
# Returns the font wrapper and font
|
479
|
-
|
480
|
-
|
527
|
+
# Returns the font wrapper, font size and font color to be used for variable text fields and
|
528
|
+
# push button captions.
|
529
|
+
def retrieve_font_information(font_name, resources)
|
481
530
|
font_object = resources.font(font_name) rescue nil
|
482
531
|
font = font_object&.font_wrapper
|
483
532
|
unless font
|
@@ -493,7 +542,7 @@ module HexaPDF
|
|
493
542
|
raise(HexaPDF::Error, "Font #{font_name} of the AcroForm's default resources not usable")
|
494
543
|
end
|
495
544
|
end
|
496
|
-
|
545
|
+
font
|
497
546
|
end
|
498
547
|
|
499
548
|
# Calculates the font size for single line text fields using auto-sizing, based on the font
|
@@ -568,12 +568,12 @@ module HexaPDF
|
|
568
568
|
seen = {} # used for combining field
|
569
569
|
|
570
570
|
validate_array = lambda do |parent, container|
|
571
|
-
container.
|
571
|
+
container.map! do |field|
|
572
572
|
if !field.kind_of?(HexaPDF::Object) || !field.kind_of?(HexaPDF::Dictionary) || field.null?
|
573
573
|
yield("Invalid object in AcroForm field hierarchy", true)
|
574
|
-
next
|
574
|
+
next nil
|
575
575
|
end
|
576
|
-
next
|
576
|
+
next field unless field.key?(:T) # Skip widgets
|
577
577
|
|
578
578
|
field = Field.wrap(document, field)
|
579
579
|
reject = false
|
@@ -597,14 +597,15 @@ module HexaPDF
|
|
597
597
|
widget[:Parent] = other_field
|
598
598
|
kids << widget
|
599
599
|
end
|
600
|
+
document.delete(field)
|
600
601
|
reject = true
|
601
602
|
elsif !reject
|
602
603
|
seen[name] = field
|
603
604
|
end
|
604
605
|
|
605
|
-
validate_array.call(field, field[:Kids]) if field.key?(:Kids)
|
606
|
-
reject
|
607
|
-
end
|
606
|
+
validate_array.call(field, field[:Kids]) if !field.null? && field.key?(:Kids)
|
607
|
+
reject ? nil : field
|
608
|
+
end.compact!
|
608
609
|
end
|
609
610
|
validate_array.call(nil, root_fields)
|
610
611
|
|
@@ -496,7 +496,7 @@ module HexaPDF
|
|
496
496
|
else
|
497
497
|
nil
|
498
498
|
end
|
499
|
-
result && (result == result.truncate ? result.to_i.to_s : result.to_s)
|
499
|
+
result && (result.finite? && result == result.truncate ? result.to_i.to_s : result.to_s)
|
500
500
|
end
|
501
501
|
|
502
502
|
AF_SIMPLE_CALCULATE_MAPPING = { #:nodoc:
|
@@ -613,7 +613,14 @@ module HexaPDF
|
|
613
613
|
|
614
614
|
# Returns the numeric value of the string, interpreting comma as point.
|
615
615
|
def af_make_number(value)
|
616
|
-
value.to_s
|
616
|
+
value = value.to_s
|
617
|
+
if value.match?(/(?:[+-])?Inf(?:inity)?/i)
|
618
|
+
value.start_with?('-') ? -Float::INFINITY : Float::INFINITY
|
619
|
+
elsif value.match?(/NaN/i)
|
620
|
+
Float::NAN
|
621
|
+
else
|
622
|
+
value.tr(',', '.').to_f
|
623
|
+
end
|
617
624
|
end
|
618
625
|
|
619
626
|
# Formats the numeric value according to the format string and separator style.
|
@@ -176,7 +176,7 @@ module HexaPDF
|
|
176
176
|
end
|
177
177
|
str = str.gsub(/[[:space:]]/, ' ') if str && concrete_field_type == :single_line_text_field
|
178
178
|
if key?(:MaxLen) && str && str.length > self[:MaxLen]
|
179
|
-
|
179
|
+
str = @document.config['acro_form.text_field.on_max_len_exceeded'].call(self, str)
|
180
180
|
end
|
181
181
|
self[:V] = str
|
182
182
|
update_widgets
|
@@ -348,7 +348,14 @@ module HexaPDF
|
|
348
348
|
return
|
349
349
|
end
|
350
350
|
if (max_len = self[:MaxLen]) && field_value && field_value.length > max_len
|
351
|
-
|
351
|
+
correctable = true
|
352
|
+
begin
|
353
|
+
str = @document.config['acro_form.text_field.on_max_len_exceeded'].call(self, field_value)
|
354
|
+
rescue HexaPDF::Error
|
355
|
+
correctable = false
|
356
|
+
end
|
357
|
+
yield("Text contents of field '#{full_field_name}' is too long", correctable)
|
358
|
+
self.field_value = str if correctable
|
352
359
|
end
|
353
360
|
if comb_text_field? && !max_len
|
354
361
|
yield("Comb text field needs a value for /MaxLen")
|
@@ -113,6 +113,18 @@ module HexaPDF
|
|
113
113
|
|
114
114
|
end
|
115
115
|
|
116
|
+
# Border effect dictionary used by square, circle and polygon annotation types.
|
117
|
+
#
|
118
|
+
# See: PDF2.0 s12.5.4
|
119
|
+
class BorderEffect < Dictionary
|
120
|
+
|
121
|
+
define_type :XXBorderEffect
|
122
|
+
|
123
|
+
define_field :S, type: Symbol, default: :S, allowed_values: [:C, :S]
|
124
|
+
define_field :I, type: Numeric, default: 0, allowed_values: [0, 1, 2]
|
125
|
+
|
126
|
+
end
|
127
|
+
|
116
128
|
extend Utils::BitField
|
117
129
|
|
118
130
|
define_type :Annot
|
@@ -133,7 +145,7 @@ module HexaPDF
|
|
133
145
|
define_field :OC, type: Dictionary, version: '1.5'
|
134
146
|
define_field :AF, type: PDFArray, version: '2.0'
|
135
147
|
define_field :ca, type: Numeric, default: 1.0, version: '2.0'
|
136
|
-
define_field :CA, type: Numeric, default: 1.0, version: '
|
148
|
+
define_field :CA, type: Numeric, default: 1.0, version: '1.4'
|
137
149
|
define_field :BM, type: Symbol, version: '2.0'
|
138
150
|
define_field :Lang, type: String, version: '2.0'
|
139
151
|
|
@@ -259,6 +271,64 @@ module HexaPDF
|
|
259
271
|
xobject
|
260
272
|
end
|
261
273
|
|
274
|
+
# Regenerates the appearance stream of the annotation.
|
275
|
+
#
|
276
|
+
# This uses the information stored in the annotation to regenerate the appearance.
|
277
|
+
#
|
278
|
+
# See: Annotations::AppearanceGenerator
|
279
|
+
def regenerate_appearance
|
280
|
+
appearance_generator_class = document.config.constantize('annotation.appearance_generator')
|
281
|
+
appearance_generator_class.new(self).create_appearance
|
282
|
+
end
|
283
|
+
|
284
|
+
# :call-seq:
|
285
|
+
# annot.contents => contents or +nil+
|
286
|
+
# annot.contents(text) => annot
|
287
|
+
#
|
288
|
+
# Returns the text of the annotation when no argument is given. Otherwise sets the text and
|
289
|
+
# returns self.
|
290
|
+
#
|
291
|
+
# The contents is used differently depending on the annotation type. It is either the text
|
292
|
+
# that should be displayed for the annotation or an alternate description of the annotation's
|
293
|
+
# contents.
|
294
|
+
#
|
295
|
+
# A value of +nil+ means deleting the existing contents entry.
|
296
|
+
def contents(text = :UNSET)
|
297
|
+
if text == :UNSET
|
298
|
+
self[:Contents]
|
299
|
+
else
|
300
|
+
self[:Contents] = text
|
301
|
+
self
|
302
|
+
end
|
303
|
+
end
|
304
|
+
|
305
|
+
# Describes the opacity values +fill_alpha+ and +stroke_alpha+ of an annotation.
|
306
|
+
#
|
307
|
+
# See Annotation#opacity
|
308
|
+
Opacity = Struct.new(:fill_alpha, :stroke_alpha)
|
309
|
+
|
310
|
+
# :call-seq:
|
311
|
+
# annotation.opacity => current_values
|
312
|
+
# annotation.opacity(fill_alpha:) => annotation
|
313
|
+
# annotation.opacity(stroke_alpha:) => annotation
|
314
|
+
# annotation.opacity(fill_alpha:, stroke_alpha:) => annotation
|
315
|
+
#
|
316
|
+
# Returns an Opacity instance representing the fill and stroke alpha values when no arguments
|
317
|
+
# are given. Otherwise sets the provided alpha values and returns self.
|
318
|
+
#
|
319
|
+
# The fill and stroke alpha values are used when regenerating the annotation's appearance
|
320
|
+
# stream and determine how opaque drawn elements will be. Note that the fill alpha value
|
321
|
+
# applies not just to fill values but to all non-stroking operations (e.g. images, ...).
|
322
|
+
def opacity(fill_alpha: nil, stroke_alpha: nil)
|
323
|
+
if !fill_alpha.nil? || !stroke_alpha.nil?
|
324
|
+
self[:CA] = stroke_alpha unless stroke_alpha.nil?
|
325
|
+
self[:ca] = fill_alpha unless fill_alpha.nil?
|
326
|
+
self
|
327
|
+
else
|
328
|
+
Opacity.new(key?(:ca) ? self[:ca] : self[:CA], self[:CA])
|
329
|
+
end
|
330
|
+
end
|
331
|
+
|
262
332
|
private
|
263
333
|
|
264
334
|
def perform_validation(&block) #:nodoc:
|
@@ -0,0 +1,348 @@
|
|
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
|
+
when :Square then create_square_circle_appearance(:square)
|
77
|
+
when :Circle then create_square_circle_appearance(:circle)
|
78
|
+
else
|
79
|
+
raise HexaPDF::Error, "Appearance regeneration for #{@annot[:Subtype]} not yet supported"
|
80
|
+
end
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
|
85
|
+
# Creates the appropriate appearance for a line annotation.
|
86
|
+
#
|
87
|
+
# Nearly all the needed information can be taken from the annotation object itself. However,
|
88
|
+
# the PDF specification doesn't specify where to take the font related information (font,
|
89
|
+
# size, alignment...) from. Therefore this is currently hard-coded as left-aligned Helvetica
|
90
|
+
# in size 9.
|
91
|
+
#
|
92
|
+
# There are also some other decisions that are left to the implementation, like padding
|
93
|
+
# around the annotation or the size of the line ending shapes. Those are implemented to be
|
94
|
+
# similar to how viewers create the appearance.
|
95
|
+
#
|
96
|
+
# See: HexaPDF::Type::Annotations::Line
|
97
|
+
def create_line_appearance
|
98
|
+
# Prepare the annotation
|
99
|
+
form = (@annot[:AP] ||= {})[:N] ||=
|
100
|
+
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 0, 0]})
|
101
|
+
form.contents = ""
|
102
|
+
@annot.flag(:print)
|
103
|
+
@annot.unflag(:hidden)
|
104
|
+
|
105
|
+
# Get or calculate all needed values from the annotation
|
106
|
+
x0, y0, x1, y1 = @annot.line
|
107
|
+
style = @annot.border_style
|
108
|
+
line_ending_style = @annot.line_ending_style
|
109
|
+
opacity = @annot.opacity
|
110
|
+
ll = @annot.leader_line_length
|
111
|
+
lle = @annot.leader_line_extension_length
|
112
|
+
llo = @annot.leader_line_offset
|
113
|
+
|
114
|
+
angle = Math.atan2(y1 - y0, x1 - x0)
|
115
|
+
cos_angle = Math.cos(angle)
|
116
|
+
sin_angle = Math.sin(angle)
|
117
|
+
line_length = Math.sqrt((y1 - y0) ** 2 + (x1 - x0) ** 2)
|
118
|
+
ll_sign = (ll > 0 ? 1 : -1)
|
119
|
+
ll_y = ll_sign * (ll.abs + lle + llo)
|
120
|
+
line_y = (ll != 0 ? ll_sign * (llo + ll.abs) : 0)
|
121
|
+
|
122
|
+
captioned = @annot.captioned
|
123
|
+
contents = @annot.contents.to_s
|
124
|
+
if captioned && !contents.empty?
|
125
|
+
cap_position = @annot.caption_position
|
126
|
+
cap_style = HexaPDF::Layout::Style.new(font: 'Helvetica', font_size: 9,
|
127
|
+
fill_color: style.color || 'black',
|
128
|
+
line_spacing: 1.25)
|
129
|
+
cap_items = @document.layout.text_fragments(contents, style: cap_style)
|
130
|
+
layouter = Layout::TextLayouter.new(cap_style)
|
131
|
+
cap_result = layouter.fit(cap_items, 2**20, 2**20)
|
132
|
+
cap_width = cap_result.lines.max_by(&:width).width + 2 # for padding left/right
|
133
|
+
cap_offset = @annot.caption_offset
|
134
|
+
|
135
|
+
cap_x = (line_length - cap_width) / 2.0 + cap_offset[0]
|
136
|
+
# Note that the '+ 2' is just so that there is a small gap to the line
|
137
|
+
cap_y = line_y + cap_offset[1] +
|
138
|
+
(cap_position == :inline ? cap_result.height / 2.0 : cap_result.height + 2)
|
139
|
+
end
|
140
|
+
|
141
|
+
# Calculate annotation rectangle and form bounding box. This considers the line's start
|
142
|
+
# and end points as well as the end points of the leader lines, the line ending style and
|
143
|
+
# the caption when calculating the bounding box.
|
144
|
+
#
|
145
|
+
# The result could still be improved by tailoring to the specific line ending style.
|
146
|
+
calculate_le_padding = lambda do |le_style|
|
147
|
+
case le_style
|
148
|
+
when :square, :circle, :diamond, :slash, :open_arrow, :closed_arrow
|
149
|
+
3 * style.width
|
150
|
+
when :ropen_arrow, :rclosed_arrow
|
151
|
+
10 * style.width
|
152
|
+
else
|
153
|
+
0
|
154
|
+
end
|
155
|
+
end
|
156
|
+
dstart = calculate_le_padding.call(line_ending_style.start_style)
|
157
|
+
dend = calculate_le_padding.call(line_ending_style.end_style)
|
158
|
+
if captioned
|
159
|
+
cap_ulx = x0 + cos_angle * cap_x - sin_angle * cap_y
|
160
|
+
cap_uly = y0 + sin_angle * cap_x + cos_angle * cap_y
|
161
|
+
end
|
162
|
+
min_x, max_x = [x0, x0 - sin_angle * ll_y, x0 - sin_angle * line_y - cos_angle * dstart,
|
163
|
+
x1, x1 - sin_angle * ll_y, x1 - sin_angle * line_y + cos_angle * dend,
|
164
|
+
*([cap_ulx,
|
165
|
+
cap_ulx + cos_angle * cap_width,
|
166
|
+
cap_ulx - sin_angle * cap_result.height,
|
167
|
+
cap_ulx + cos_angle * cap_width - sin_angle * cap_result.height
|
168
|
+
] if captioned)
|
169
|
+
].minmax
|
170
|
+
min_y, max_y = [y0, y0 + cos_angle * ll_y,
|
171
|
+
y0 + cos_angle * line_y - ([cos_angle, sin_angle].max) * dstart,
|
172
|
+
y1, y1 + cos_angle * ll_y,
|
173
|
+
y1 + cos_angle * line_y + ([cos_angle, sin_angle].max) * dend,
|
174
|
+
*([cap_uly,
|
175
|
+
cap_uly + sin_angle * cap_width,
|
176
|
+
cap_uly - cos_angle * cap_result.height,
|
177
|
+
cap_uly + sin_angle * cap_width - cos_angle * cap_result.height
|
178
|
+
] if captioned)
|
179
|
+
].minmax
|
180
|
+
|
181
|
+
padding = 4 * style.width
|
182
|
+
rect = [min_x - padding, min_y - padding, max_x + padding, max_y + padding]
|
183
|
+
@annot[:Rect] = rect
|
184
|
+
form[:BBox] = rect.dup
|
185
|
+
|
186
|
+
# Set the appropriate graphics state and transform the canvas so that the line is
|
187
|
+
# unrotated and its start point at the origin.
|
188
|
+
canvas = form.canvas(translate: false)
|
189
|
+
canvas.opacity(**opacity.to_h)
|
190
|
+
canvas.stroke_color(style.color) if style.color
|
191
|
+
canvas.fill_color(@annot.interior_color) if @annot.interior_color
|
192
|
+
canvas.line_width(style.width)
|
193
|
+
canvas.line_dash_pattern(style.style) if style.style.kind_of?(Array)
|
194
|
+
canvas.transform(cos_angle, sin_angle, -sin_angle, cos_angle, x0, y0)
|
195
|
+
|
196
|
+
stroke_op = (style.color ? :stroke : :end_path)
|
197
|
+
fill_op = (style.color && @annot.interior_color ? :fill_stroke :
|
198
|
+
(style.color ? :stroke : :fill))
|
199
|
+
|
200
|
+
# Draw leader lines and line
|
201
|
+
if ll != 0
|
202
|
+
canvas.line(0, ll_sign * llo, 0, ll_y)
|
203
|
+
canvas.line(line_length, ll_sign * llo, line_length, ll_y)
|
204
|
+
end
|
205
|
+
if captioned && cap_position == :inline
|
206
|
+
canvas.line(0, line_y, [[0, cap_x].max, line_length].min, line_y)
|
207
|
+
canvas.line([[cap_x + cap_width, 0].max, line_length].min, line_y, line_length, line_y)
|
208
|
+
else
|
209
|
+
canvas.line(0, line_y, line_length, line_y)
|
210
|
+
end
|
211
|
+
canvas.send(stroke_op)
|
212
|
+
|
213
|
+
# Draw line endings
|
214
|
+
if line_ending_style.start_style != :none
|
215
|
+
do_fill = draw_line_ending(canvas, line_ending_style.start_style, 0, line_y,
|
216
|
+
style.width, 0)
|
217
|
+
canvas.send(do_fill ? fill_op : stroke_op)
|
218
|
+
end
|
219
|
+
if line_ending_style.end_style != :none
|
220
|
+
do_fill = draw_line_ending(canvas, line_ending_style.end_style, line_length, line_y,
|
221
|
+
style.width, Math::PI)
|
222
|
+
canvas.send(do_fill ? fill_op : stroke_op)
|
223
|
+
end
|
224
|
+
|
225
|
+
# Draw caption, adding half of the padding added to cap_width
|
226
|
+
cap_result.draw(canvas, cap_x + 1, cap_y) if captioned
|
227
|
+
end
|
228
|
+
|
229
|
+
# Creates the appropriate appearance for a square or circle annotation depending on the
|
230
|
+
# given +type+ (which can either be +:square+ or +:circle+).
|
231
|
+
#
|
232
|
+
# The cloudy border effect is not supported.
|
233
|
+
#
|
234
|
+
# See: HexaPDF::Type::Annotations::Square, HexaPDF::Type::Annotations::Circle
|
235
|
+
def create_square_circle_appearance(type)
|
236
|
+
# Prepare the annotation
|
237
|
+
form = (@annot[:AP] ||= {})[:N] ||=
|
238
|
+
@document.add({Type: :XObject, Subtype: :Form, BBox: [0, 0, 0, 0]})
|
239
|
+
form.contents = ""
|
240
|
+
@annot.flag(:print)
|
241
|
+
@annot.unflag(:hidden)
|
242
|
+
|
243
|
+
rect = @annot[:Rect]
|
244
|
+
x, y, w, h = rect.left, rect.bottom, rect.width, rect.height
|
245
|
+
border_style = @annot.border_style
|
246
|
+
interior_color = @annot.interior_color
|
247
|
+
opacity = @annot.opacity
|
248
|
+
|
249
|
+
# Take the differences array into account. If it exists, the boundary of the actual
|
250
|
+
# rectangle is the one with the differences applied to /Rect.
|
251
|
+
#
|
252
|
+
# If the differences array doesn't exist, we assume that the /Rect is the rectangle we
|
253
|
+
# want to draw, with the line width split on both side (like with Canvas#rectangle). In
|
254
|
+
# this case we need to update /Rect accordingly so that the line width on the outside is
|
255
|
+
# correctly shown.
|
256
|
+
line_width_adjustment = border_style.width / 2.0
|
257
|
+
if (rd = @annot[:RD])
|
258
|
+
x += rd[0]
|
259
|
+
y += rd[3]
|
260
|
+
w -= rd[0] + rd[2]
|
261
|
+
h -= rd[1] + rd[3]
|
262
|
+
else
|
263
|
+
@annot[:RD] = [0, 0, 0, 0]
|
264
|
+
x = rect.left -= line_width_adjustment
|
265
|
+
y = rect.bottom -= line_width_adjustment
|
266
|
+
w = rect.width += line_width_adjustment
|
267
|
+
h = rect.height += line_width_adjustment
|
268
|
+
end
|
269
|
+
x += line_width_adjustment
|
270
|
+
y += line_width_adjustment
|
271
|
+
w -= 2 * line_width_adjustment
|
272
|
+
h -= 2 * line_width_adjustment
|
273
|
+
|
274
|
+
x -= rect.left
|
275
|
+
y -= rect.bottom
|
276
|
+
form[:BBox] = [0, 0, rect.width, rect.height]
|
277
|
+
|
278
|
+
if border_style.color || interior_color
|
279
|
+
canvas = form.canvas
|
280
|
+
canvas.opacity(**opacity.to_h)
|
281
|
+
canvas.stroke_color(border_style.color) if border_style.color
|
282
|
+
canvas.fill_color(interior_color) if interior_color
|
283
|
+
canvas.line_width(border_style.width)
|
284
|
+
canvas.line_dash_pattern(border_style.style) if border_style.style.kind_of?(Array)
|
285
|
+
|
286
|
+
if type == :square
|
287
|
+
canvas.rectangle(x, y, w, h)
|
288
|
+
else
|
289
|
+
canvas.ellipse(x + w / 2.0, y + h / 2.0, a: w / 2.0, b: h / 2.0)
|
290
|
+
end
|
291
|
+
|
292
|
+
if border_style.color && interior_color
|
293
|
+
canvas.fill_stroke
|
294
|
+
elsif border_style.color
|
295
|
+
canvas.stroke
|
296
|
+
else
|
297
|
+
canvas.fill
|
298
|
+
end
|
299
|
+
end
|
300
|
+
end
|
301
|
+
|
302
|
+
# Draws the line ending style +type+ at the position (+x+, +y+) and returns +true+ if the
|
303
|
+
# shape needs to be filled.
|
304
|
+
#
|
305
|
+
# The argument +angle+ specifies the angle at which the line ending style should be drawn.
|
306
|
+
#
|
307
|
+
# The +line_width+ is needed because the size of the line ending depends on it.
|
308
|
+
def draw_line_ending(canvas, type, x, y, line_width, angle)
|
309
|
+
lw3 = 3 * line_width
|
310
|
+
|
311
|
+
case type
|
312
|
+
when :square
|
313
|
+
canvas.rectangle(x - lw3, y - lw3, 2 * lw3, 2 * lw3)
|
314
|
+
true
|
315
|
+
when :circle
|
316
|
+
canvas.circle(x, y, lw3)
|
317
|
+
true
|
318
|
+
when :diamond
|
319
|
+
canvas.polygon(x + lw3, y, x, y + lw3, x - lw3, y, x, y - lw3)
|
320
|
+
true
|
321
|
+
when :open_arrow, :closed_arrow, :ropen_arrow, :rclosed_arrow
|
322
|
+
arrow_cos = Math.cos(Math::PI / 6 + angle)
|
323
|
+
arrow_sin = Math.sin(Math::PI / 6 + angle)
|
324
|
+
dir = (type == :ropen_arrow || type == :rclosed_arrow ? -1 : 1)
|
325
|
+
canvas.polyline(x + dir * arrow_cos * 3 * lw3, y + arrow_sin * 3 * lw3, x, y,
|
326
|
+
x + dir * arrow_cos * 3 * lw3, y - arrow_sin * 3 * lw3)
|
327
|
+
if type == :closed_arrow || type == :rclosed_arrow
|
328
|
+
canvas.close_subpath
|
329
|
+
true
|
330
|
+
else
|
331
|
+
false
|
332
|
+
end
|
333
|
+
when :butt
|
334
|
+
canvas.line(x, y + lw3, x, y - lw3)
|
335
|
+
false
|
336
|
+
when :slash
|
337
|
+
sin_60 = Math.sin(Math::PI / 3)
|
338
|
+
cos_60 = Math.cos(Math::PI / 3)
|
339
|
+
canvas.line(x + cos_60 * lw3, y + sin_60 * lw3, x - cos_60 * lw3, y - sin_60 * lw3)
|
340
|
+
false
|
341
|
+
end
|
342
|
+
end
|
343
|
+
|
344
|
+
end
|
345
|
+
|
346
|
+
end
|
347
|
+
end
|
348
|
+
end
|