hexapdf 0.25.0 → 0.26.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0f8bf550cff0d50567439c9942c97d6fc32e9fa9edab3bbff05ce854e22b0cc3
4
- data.tar.gz: bab6243ec81278f278589fc90768dd7e2c6c73c0c314bd8ce0f17350f2d6cd73
3
+ metadata.gz: 9e41c50e1ddb3dc2b063e8a858ccf612fb069df46189d592dccec3778d58d738
4
+ data.tar.gz: cf62d302bd4728816c9920d09fcf27224004b6e70bc23b19d9a0f8b37dc27a3b
5
5
  SHA512:
6
- metadata.gz: bfdfa30c16cae575fb028b9f804516601c56e02dfcf77b63452d63a9ce3ce28950661ef0de3b5a178fef7442fee51ef911e798b627f627e8f1991db6ae185691
7
- data.tar.gz: aea9f97cb7465cc9592cab25e11c890efce3d10fa0c6c69dcaade78fc94a68fef1b1dd907f87c96f894840d913342d7183ad1efc6fd6d6e8be290759168af105
6
+ metadata.gz: 83afdc841024adb116efbeb75bd34c2feb71042cbccb368e32b220634f3f8122f1a29bf07d8b7532836953fe1d3edb0733cb6c31fd50177a28bddea9bcdee2a2
7
+ data.tar.gz: fa3e26f634fc7e3e6461ade1555a063bffbf52c4e956d802b4bee5d01290bc2415571501b4afa083aaefd9f514a38a10fb10546418dbd5b7ef9bfbd121abd37f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,23 @@
1
+ ## 0.26.0 - 2022-10-14
2
+
3
+ ### Added
4
+
5
+ * Support for page labels
6
+ * [HexaPDF::Type::MarkInformation]
7
+
8
+ ### Changed
9
+
10
+ * [HexaPDF::Rectangle] to recover from invalid values by defaulting to
11
+ `[0, 0, 0, 0]`
12
+
13
+ ### Fixed
14
+
15
+ * [HexaPDF::DictionaryFields::PDFByteStringConverter] to duplicate the string
16
+ before conversion
17
+ * [HexaPDF::Type::FileSpecification#path=] to duplicate the given string value
18
+ due to using it for two different fields
19
+
20
+
1
21
  ## 0.25.0 - 2022-10-02
2
22
 
3
23
  ### Added
@@ -588,6 +588,8 @@ module HexaPDF
588
588
  TransformParams: 'HexaPDF::Type::Signature::TransformParams',
589
589
  Outlines: 'HexaPDF::Type::Outline',
590
590
  XXOutlineItem: 'HexaPDF::Type::OutlineItem',
591
+ PageLabel: 'HexaPDF::Type::PageLabel',
592
+ XXMarkInformation: 'HexaPDF::Type::MarkInformation',
591
593
  },
592
594
  'object.subtype_map' => {
593
595
  nil => {
@@ -268,7 +268,7 @@ module HexaPDF
268
268
  # returns +nil+.
269
269
  def self.convert(str, _type, _document)
270
270
  return if !str.kind_of?(String) || str.encoding == Encoding::BINARY
271
- str.force_encoding(Encoding::BINARY)
271
+ str.dup.force_encoding(Encoding::BINARY)
272
272
  end
273
273
 
274
274
  end
@@ -41,8 +41,26 @@ module HexaPDF
41
41
 
42
42
  # This class provides methods for managing the pages of a PDF file.
43
43
  #
44
- # It uses the methods of HexaPDF::Type::PageTreeNode underneath but provides a more convenient
45
- # interface.
44
+ # For page manipulation it uses the methods of HexaPDF::Type::PageTreeNode underneath but
45
+ # provides a more convenient interface.
46
+ #
47
+ # == Page Labels
48
+ #
49
+ # In addition to page manipulation, the class provides methods for managing the page labels
50
+ # which are alternative descriptions for the pages. In contrast to the page indices which are
51
+ # fixed the page labels can be freely defined.
52
+ #
53
+ # The way this works is that one can assign page label objects (HexaPDF::Type::PageLabel) to
54
+ # page ranges via the /PageLabels number tree in the catalog. The page label objects specify how
55
+ # the pages in their range shall be labeled. See HexaPDF::Type::PageLabel for examples of page
56
+ # labels.
57
+ #
58
+ # To facilitate the easy use of page labels the following methods are provided:
59
+ #
60
+ # * #page_label
61
+ # * #each_labelling_range
62
+ # * #add_labelling_range
63
+ # * #delete_labelling_range
46
64
  class Pages
47
65
 
48
66
  include Enumerable
@@ -154,6 +172,85 @@ module HexaPDF
154
172
  alias size count
155
173
  alias length count
156
174
 
175
+ # Returns the constructed page label for the given page index.
176
+ #
177
+ # If no page labels are defined, +nil+ is returned.
178
+ #
179
+ # See HexaPDF::Type::PageLabel for examples.
180
+ def page_label(page_index)
181
+ raise(ArgumentError, 'Page index out of range') if page_index < 0 || page_index >= count
182
+ each_labelling_range do |index, count, label|
183
+ if page_index < index + count
184
+ return label.construct_label(page_index - index)
185
+ end
186
+ end
187
+ end
188
+
189
+ # :call-seq:
190
+ # pages.each_labelling_range {|first_index, count, page_label| block } -> pages
191
+ # pages.each_labelling_range -> Enumerator
192
+ #
193
+ # Iterates over all defined labelling ranges inorder, yielding the page index of the first
194
+ # page in the labelling range, the number of pages in the range, and the associated page label
195
+ # object.
196
+ #
197
+ # The last yielded count might be equal or lower than zero in case the document has fewer
198
+ # pages than anticipated by the labelling ranges.
199
+ def each_labelling_range
200
+ return to_enum(__method__) unless block_given?
201
+ return unless @document.catalog.page_labels
202
+
203
+ last_start = nil
204
+ last_label = nil
205
+ @document.catalog.page_labels.each_entry do |s1, p1|
206
+ yield(last_start, s1 - last_start, @document.wrap(last_label, type: :PageLabel)) if last_start
207
+ last_start = s1
208
+ last_label = p1
209
+ end
210
+ if last_start
211
+ yield(last_start, count - last_start, @document.wrap(last_label, type: :PageLabel))
212
+ end
213
+
214
+ self
215
+ end
216
+
217
+ # Adds a new labelling range starting at +start_index+ and returns it.
218
+ #
219
+ # See HexaPDF::Type::PageLabel for information on the arguments +numbering_style+, +prefix+,
220
+ # and +start_number+.
221
+ #
222
+ # If a labelling range already exists for the given +start_index+, its value will be
223
+ # overwritten.
224
+ #
225
+ # If there are no existing labelling ranges and the given +start_index+ isn't 0, a default
226
+ # labelling range using start index 0 and numbering style :decimal is added.
227
+ def add_labelling_range(start_index, numbering_style: nil, prefix: nil, start_number: nil)
228
+ page_label = @document.wrap({}, type: :PageLabel)
229
+ page_label.numbering_style(numbering_style) if numbering_style
230
+ page_label.prefix(prefix) if prefix
231
+ page_label.start_number(start_number) if start_number
232
+
233
+ labels = @document.catalog.page_labels(create: true)
234
+ labels.add_entry(start_index, page_label)
235
+ labels.add_entry(0, {S: :d}) unless labels.find_entry(0)
236
+
237
+ page_label
238
+ end
239
+
240
+ # Deletes the page labelling range starting at +start_index+ and returns the associated page
241
+ # label object.
242
+ #
243
+ # Note: The page label for the range starting at zero can only be deleted last!
244
+ def delete_labelling_range(start_index)
245
+ return unless (labels = @document.catalog.page_labels)
246
+ if start_index == 0 && labels.each_entry.first(2).size == 2
247
+ raise HexaPDF::Error, "Page labelling range starting at 0 must be deleted last"
248
+ end
249
+ page_label = labels.delete_entry(start_index)
250
+ @document.catalog.delete(:PageLabels) if start_index == 0
251
+ page_label
252
+ end
253
+
157
254
  end
158
255
 
159
256
  end
@@ -116,12 +116,19 @@ module HexaPDF
116
116
 
117
117
  private
118
118
 
119
+ #:nodoc:
120
+ RECTANGLE_ERROR_MSG = "A PDF rectangle structure must contain an array of four numbers"
121
+
119
122
  # Ensures that the value is an array containing four numbers that specify the bottom left and
120
123
  # top right corner.
121
124
  def after_data_change
122
125
  super
123
126
  unless value.size == 4 && all? {|v| v.kind_of?(Numeric) }
124
- raise ArgumentError, "A PDF rectangle structure must contain an array of four numbers"
127
+ if !document? ||
128
+ document.config['parser.on_correctable_error'].call(document, RECTANGLE_ERROR_MSG, 0)
129
+ raise ArgumentError, RECTANGLE_ERROR_MSG
130
+ end
131
+ value.replace([0, 0, 0, 0])
125
132
  end
126
133
  self[0], self[2] = self[2], self[0] if self[0] > self[2]
127
134
  self[1], self[3] = self[3], self[1] if self[1] > self[3]
@@ -130,7 +137,9 @@ module HexaPDF
130
137
  def perform_validation #:nodoc:
131
138
  super
132
139
  unless value.size == 4 && all? {|v| v.kind_of?(Numeric) }
133
- yield("A PDF rectangle structure must contain an array of four numbers", false)
140
+ yield("A PDF rectangle structure must contain an array of four numbers; replacing " \
141
+ "it with [0, 0, 0, 0]", true)
142
+ value.replace([0, 0, 0, 0])
134
143
  end
135
144
  end
136
145
 
@@ -73,7 +73,7 @@ module HexaPDF
73
73
  define_field :AcroForm, type: :XXAcroForm, version: '1.2'
74
74
  define_field :Metadata, type: Stream, indirect: true, version: '1.4'
75
75
  define_field :StructTreeRoot, type: Dictionary, version: '1.3'
76
- define_field :MarkInfo, type: Dictionary, version: '1.4'
76
+ define_field :MarkInfo, type: :XXMarkInformation, version: '1.4'
77
77
  define_field :Lang, type: String, version: '1.4'
78
78
  define_field :SpiderInfo, type: Dictionary, version: '1.3'
79
79
  define_field :OutputIntents, type: PDFArray, version: '1.4'
@@ -132,6 +132,23 @@ module HexaPDF
132
132
  end
133
133
  end
134
134
 
135
+ # Returns the page labels number tree.
136
+ #
137
+ # * If a page labels number tree exists, the +create+ argument is not used.
138
+ #
139
+ # * If no page labels number tree exists and +create+ is +true+, a new one is created.
140
+ #
141
+ # * If no page labels number tree exists and +create+ is +false+, +nil+ is returned.
142
+ #
143
+ # See: HexaPDF::Document::Pages
144
+ def page_labels(create: false)
145
+ if (object = self[:PageLabels])
146
+ object
147
+ elsif create
148
+ self[:PageLabels] = document.wrap({}, type: NumberTreeNode)
149
+ end
150
+ end
151
+
135
152
  private
136
153
 
137
154
  # Ensures that there is a valid page tree.
@@ -116,7 +116,8 @@ module HexaPDF
116
116
  #
117
117
  # Since the /Unix, /Mac and /DOS fields are obsolescent, only the /F and /UF fields are set.
118
118
  def path=(filename)
119
- self[:UF] = self[:F] = filename
119
+ self[:UF] = filename
120
+ self[:F] = filename.b
120
121
  delete(:FS)
121
122
  delete(:Unix)
122
123
  delete(:Mac)
@@ -0,0 +1,57 @@
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-2022 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/dictionary'
38
+
39
+ module HexaPDF
40
+ module Type
41
+
42
+ # Represents the mark information dictionary which provides some general information related to
43
+ # structured PDF documents.
44
+ #
45
+ # See: PDF1.7 s14.7.1
46
+ class MarkInformation < Dictionary
47
+
48
+ define_type :XXMarkInformation
49
+
50
+ define_field :Marked, type: Boolean, default: false
51
+ define_field :UserProperties, type: Boolean, default: false
52
+ define_field :Suspects, type: Boolean, default: false
53
+
54
+ end
55
+
56
+ end
57
+ end
@@ -353,6 +353,14 @@ module HexaPDF
353
353
  idx
354
354
  end
355
355
 
356
+ # Returns the label of the page which is an optional, alternative description of the page
357
+ # index.
358
+ #
359
+ # See HexaPDF::Document::Pages for details.
360
+ def label
361
+ document.pages.page_label(index)
362
+ end
363
+
356
364
  # Returns all parent nodes of the page up to the root of the page tree.
357
365
  #
358
366
  # The direct parent is the first node in the array and the root node the last.
@@ -0,0 +1,222 @@
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-2022 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/dictionary'
38
+
39
+ module HexaPDF
40
+ module Type
41
+
42
+ # Represents a page label dictionary.
43
+ #
44
+ # A page label dictionary contains information about the numbering style, the label prefix and
45
+ # the start number to construct page labels like 'A-1' or 'iii'. What is not stored is the page
46
+ # to which it is applied since that is stored in a number tree referenced through the
47
+ # /PageLabels entry in the document catalog.
48
+ #
49
+ # See HexaPDF::Document::Pages for details on how to create and manage page labels.
50
+ #
51
+ # Examples:
52
+ #
53
+ # * numbering style :decimal, prefix none, start number default value
54
+ #
55
+ # 1, 2, 3, 4, ...
56
+ #
57
+ # * numbering style :lowercase_letters, prefix 'Appendix ', start number 5
58
+ #
59
+ # Appendix e, Appendix f, Appendix g, ...
60
+ #
61
+ # * numbering style :uppercase_roman, prefix none, start number 10
62
+ #
63
+ # X, XI, XII, XIII, ...
64
+ #
65
+ # * numbering style :none, prefix 'Page', start number default value
66
+ #
67
+ # Page, Page, Page, Page, ...
68
+ #
69
+ # * numbering style :none, prefix none, start number default value
70
+ #
71
+ # "", "", "", ... (i.e. always the empty string)
72
+ #
73
+ # See: PDF1.7 s12.4.2, HexaPDF::Document::Pages, HexaPDF::Type::Catalog
74
+ class PageLabel < Dictionary
75
+
76
+ define_type :PageLabel
77
+
78
+ define_field :Type, type: Symbol, default: type
79
+ define_field :S, type: Symbol, allowed_values: [:D, :R, :r, :A, :a]
80
+ define_field :P, type: String
81
+ define_field :St, type: Integer, default: 1
82
+
83
+ # Constructs the page label for the given index which needs to be relative to the page index
84
+ # of the first page in the associated labelling range.
85
+ #
86
+ # This method is usually not called directly but through HexaPDF::Document::Pages#page_label.
87
+ def construct_label(index)
88
+ label = (prefix || '').dup
89
+ number = start_number + index
90
+ case numbering_style
91
+ when :decimal
92
+ label + number.to_s
93
+ when :uppercase_roman
94
+ label + number_to_roman_numeral(number)
95
+ when :lowercase_roman
96
+ label + number_to_roman_numeral(number, lowercase: true)
97
+ when :uppercase_letters
98
+ label + number_to_letters(number)
99
+ when :lowercase_letters
100
+ label + number_to_letters(number, lowercase: true)
101
+ when :none
102
+ label
103
+ end
104
+ end
105
+
106
+ # :nodoc:
107
+ NUMBERING_STYLE_MAPPING = {
108
+ decimal: :D, D: :D,
109
+ uppercase_roman: :R, R: :R,
110
+ lowercase_roman: :r, r: :r,
111
+ uppercase_letters: :A, A: :A,
112
+ lowercase_letters: :a, a: :a,
113
+ none: nil
114
+ }
115
+
116
+ # :nodoc:
117
+ REVERSE_NUMBERING_STYLE_MAPPING = Hash[*NUMBERING_STYLE_MAPPING.flatten.reverse]
118
+
119
+ # :call-seq:
120
+ # page_label.numbering_style -> numbering_style
121
+ # page_label.numbering_style(value) -> numbering_style
122
+ #
123
+ # Returns the numbering style if no argument is given. Otherwise sets the numbering style to
124
+ # the given value.
125
+ #
126
+ # The following numbering styles are available:
127
+ #
128
+ # :none:: No numbering is done; the label only consists of the prefix.
129
+ # :decimal:: Decimal arabic numerals (1, 2, 3, 4, ...).
130
+ # :uppercase_roman:: Uppercase roman numerals (I, II, III, IV, ...)
131
+ # :lowercase_roman:: Lowercase roman numerals (i, ii, iii, iv, ...)
132
+ # :uppercase_letters:: Uppercase letters (A, B, C, D, ...)
133
+ # :lowercase_letters:: Lowercase letters (a, b, c, d, ...)
134
+ def numbering_style(value = nil)
135
+ if value
136
+ self[:S] = NUMBERING_STYLE_MAPPING.fetch(value) do
137
+ raise ArgumentError, "Invalid numbering style specified: #{value}"
138
+ end
139
+ else
140
+ REVERSE_NUMBERING_STYLE_MAPPING.fetch(self[:S], :none)
141
+ end
142
+ end
143
+
144
+ # :call-seq:
145
+ # page_label.prefix -> prefix
146
+ # page_label.prefix(value) -> prefix
147
+ #
148
+ # Returns the label prefix if no argument is given. Otherwise sets the label prefix to the
149
+ # given string value.
150
+ def prefix(value = nil)
151
+ if value
152
+ self[:P] = value
153
+ else
154
+ self[:P]
155
+ end
156
+ end
157
+
158
+ # :call-seq:
159
+ # page_label.start_number -> start_number
160
+ # page_label.start_number(value) -> start_number
161
+ #
162
+ # Returns the start number if no argument is given. Otherwise sets the start number to the
163
+ # given integer value.
164
+ def start_number(value = nil)
165
+ if value
166
+ if !value.kind_of?(Integer) || value < 1
167
+ raise ArgumentError, "Start number must be an integer greater than or equal to 1"
168
+ end
169
+ self[:St] = value
170
+ else
171
+ self[:St]
172
+ end
173
+ end
174
+
175
+ private
176
+
177
+ # :nodoc:
178
+ ALPHABET = ('A'..'Z').to_a
179
+
180
+ # Maps the given number to uppercase (or, if +lowercase+ is +true+, lowercase) letters (e.g. 1
181
+ # -> A, 27 -> AA, 28 -> AB, ...).
182
+ def number_to_letters(number, lowercase: false)
183
+ result = "".dup
184
+ while number > 0
185
+ number, rest = (number - 1).divmod(26)
186
+ result.prepend(ALPHABET[rest])
187
+ end
188
+ lowercase ? result.downcase : result
189
+ end
190
+
191
+ # :nodoc:
192
+ ROMAN_NUMERAL_MAPPING = {
193
+ 1000 => "M",
194
+ 900 => "CM",
195
+ 500 => "D",
196
+ 400 => "CD",
197
+ 100 => "C",
198
+ 90 => "XC",
199
+ 50 => "L",
200
+ 40 => "XL",
201
+ 10 => "X",
202
+ 9 => "IX",
203
+ 5 => "V",
204
+ 4 => "IV",
205
+ 1 => "I",
206
+ }
207
+
208
+ # Maps the given number to an uppercase (or, if +lowercase+ is +true+, lowercase) roman
209
+ # numeral.
210
+ def number_to_roman_numeral(number, lowercase: false)
211
+ result = ROMAN_NUMERAL_MAPPING.inject("".dup) do |memo, (base, roman_numeral)|
212
+ next memo if number < base
213
+ quotient, number = number.divmod(base)
214
+ memo << roman_numeral * quotient
215
+ end
216
+ lowercase ? result.downcase : result
217
+ end
218
+
219
+ end
220
+
221
+ end
222
+ end
data/lib/hexapdf/type.rb CHANGED
@@ -75,6 +75,8 @@ module HexaPDF
75
75
  autoload(:Signature, 'hexapdf/type/signature')
76
76
  autoload(:Outline, 'hexapdf/type/outline')
77
77
  autoload(:OutlineItem, 'hexapdf/type/outline_item')
78
+ autoload(:PageLabel, 'hexapdf/type/page_label')
79
+ autoload(:MarkInformation, 'hexapdf/type/mark_information')
78
80
 
79
81
  end
80
82
 
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.25.0'
40
+ VERSION = '0.26.0'
41
41
 
42
42
  end
@@ -136,4 +136,90 @@ describe HexaPDF::Document::Pages do
136
136
  assert_equal(3, @doc.pages.count)
137
137
  end
138
138
  end
139
+
140
+ describe "page_label" do
141
+ it "returns the page label object for the given range start index" do
142
+ 11.times { @doc.pages.add }
143
+ @doc.catalog[:PageLabels] = {Nums: [0, {S: :D}, 5, {S: :r, St: 2}, 10, {P: 'A-', S: :a}]}
144
+ assert_equal("1", @doc.pages.page_label(0))
145
+ assert_equal("5", @doc.pages.page_label(4))
146
+ assert_equal("ii", @doc.pages.page_label(5))
147
+ assert_equal("vi", @doc.pages.page_label(9))
148
+ assert_equal("A-a", @doc.pages.page_label(10))
149
+ end
150
+
151
+ it "fails if the page index is out of range" do
152
+ assert_raises(ArgumentError) { @doc.pages.page_label(-1) }
153
+ assert_raises(ArgumentError) { @doc.pages.page_label(0) }
154
+ end
155
+ end
156
+
157
+ describe "each_labelling_range" do
158
+ before do
159
+ 10.times { @doc.pages.add }
160
+ end
161
+
162
+ it "returns no entries for an empty or non-existing /PageLabels entry" do
163
+ assert(@doc.pages.each_labelling_range.to_a.empty?)
164
+ end
165
+
166
+ it "works for a single page label entry" do
167
+ @doc.catalog[:PageLabels] = {Nums: [0, {S: :r}]}
168
+ result = @doc.pages.each_labelling_range.to_a
169
+ assert_equal([[0, 10, {S: :r}]], result.map {|s, c, l| [s, c, l.value]})
170
+ assert_equal(:lowercase_roman, result[0].last.numbering_style)
171
+ end
172
+
173
+ it "works for multiple page label entries" do
174
+ @doc.catalog[:PageLabels] = {Nums: [0, {S: :r}, 2, {S: :d}, 7, {S: :A}]}
175
+ result = @doc.pages.each_labelling_range.to_a
176
+ assert_equal([[0, 2, {S: :r}], [2, 5, {S: :d}], [7, 3, {S: :A}]],
177
+ result.map {|s, c, l| [s, c, l.value]})
178
+ end
179
+
180
+ it "returns a zero or negative count for the last range if there aren't enough pages" do
181
+ assert_equal(10, @doc.pages.count)
182
+ @doc.catalog[:PageLabels] = {Nums: [0, {S: :d}, 10, {S: :r}]}
183
+ assert_equal(0, @doc.pages.each_labelling_range.to_a[-1][1])
184
+ @doc.catalog[:PageLabels][:Nums][2] = 11
185
+ assert_equal(-1, @doc.pages.each_labelling_range.to_a[-1][1])
186
+ end
187
+ end
188
+
189
+ describe "add_labelling_range" do
190
+ it "creates a new page label object for the given arguments" do
191
+ label = @doc.pages.add_labelling_range(5, numbering_style: :lowercase_roman,
192
+ start_number: 5, prefix: 'a')
193
+ assert_equal({S: :r, St: 5, P: 'a'}, label.value)
194
+ assert_equal(label, @doc.catalog.page_labels.find_entry(5))
195
+ end
196
+
197
+ it "adds an entry for the range starting at 0 if it doesn't exist" do
198
+ label = @doc.pages.add_labelling_range(5)
199
+ assert_equal([{S: :d}, label],
200
+ @doc.catalog.page_labels[:Nums].value.values_at(1, 3))
201
+ end
202
+ end
203
+
204
+ describe "delete_labelling_range" do
205
+ before do
206
+ @doc.catalog[:PageLabels] = {Nums: [0, {S: :r}, 5, {S: :d}]}
207
+ end
208
+
209
+ it "deletes the labelling range for a given start index" do
210
+ label = @doc.pages.delete_labelling_range(5)
211
+ assert_equal({S: :d}, label)
212
+ end
213
+
214
+ it "deletes the labelling range for 0 if it is the last, together with the number tree" do
215
+ @doc.pages.delete_labelling_range(5)
216
+ label = @doc.pages.delete_labelling_range(0)
217
+ assert_equal({S: :r}, label)
218
+ assert_nil(@doc.catalog[:PageLabels])
219
+ end
220
+
221
+ it "fails if the range starting at zero is deleted when other ranges still exist" do
222
+ assert_raises(HexaPDF::Error) { @doc.pages.delete_labelling_range(0) }
223
+ end
224
+ end
139
225
  end
@@ -156,8 +156,10 @@ describe HexaPDF::DictionaryFields do
156
156
  it "allows conversion to a binary string" do
157
157
  refute(@field.convert('test'.b, self))
158
158
 
159
- str = @field.convert("test", self)
159
+ input = "test"
160
+ str = @field.convert(input, self)
160
161
  assert_equal('test', str)
162
+ refute_same(input, str)
161
163
  assert_equal(Encoding::BINARY, str.encoding)
162
164
  end
163
165
  end
@@ -6,11 +6,23 @@ require 'hexapdf/document'
6
6
 
7
7
  describe HexaPDF::Rectangle do
8
8
  describe "after_data_change" do
9
- it "fails if the rectangle doesn't contain four numbers" do
9
+ it "fails if the rectangle doesn't contain four numbers, without document" do
10
10
  assert_raises(ArgumentError) { HexaPDF::Rectangle.new([1, 2, 3]) }
11
11
  assert_raises(ArgumentError) { HexaPDF::Rectangle.new([1, 2, 3, :a]) }
12
12
  end
13
13
 
14
+ it "fails if the rectangle doesn't contain four numbers, with document and strict mode" do
15
+ doc = HexaPDF::Document.new(config: {'parser.on_correctable_error' => lambda { true }})
16
+ assert_raises(ArgumentError) { HexaPDF::Rectangle.new([1, 2, 3], document: doc) }
17
+ assert_raises(ArgumentError) { HexaPDF::Rectangle.new([1, 2, 3, :a], document: doc) }
18
+ end
19
+
20
+ it "recovers if the rectangle doesn't contain four numbers, with document default mode" do
21
+ doc = HexaPDF::Document.new
22
+ assert_equal([0, 0, 0, 0], HexaPDF::Rectangle.new([1, 2, 3], document: doc).value)
23
+ assert_equal([0, 0, 0, 0], HexaPDF::Rectangle.new([1, 2, 3, :a], document: doc).value)
24
+ end
25
+
14
26
  it "normalizes the array values" do
15
27
  rect = HexaPDF::Rectangle.new([0, 1, 2, 3])
16
28
  assert_equal([0, 1, 2, 3], rect.value)
@@ -57,10 +69,12 @@ describe HexaPDF::Rectangle do
57
69
  assert(rect.validate)
58
70
 
59
71
  rect.value.shift
60
- refute(rect.validate)
72
+ assert(rect.validate)
73
+ assert_equal([0, 0, 0, 0], rect.value)
61
74
 
62
- rect.value.unshift(:A)
63
- refute(rect.validate)
75
+ rect.value[-1] = :A
76
+ assert(rect.validate)
77
+ assert_equal([0, 0, 0, 0], rect.value)
64
78
  end
65
79
  end
66
80
  end
@@ -40,7 +40,7 @@ describe HexaPDF::Writer do
40
40
  219
41
41
  %%EOF
42
42
  3 0 obj
43
- <</Producer(HexaPDF version 0.25.0)>>
43
+ <</Producer(HexaPDF version 0.26.0)>>
44
44
  endobj
45
45
  xref
46
46
  3 1
@@ -72,7 +72,7 @@ describe HexaPDF::Writer do
72
72
  141
73
73
  %%EOF
74
74
  6 0 obj
75
- <</Producer(HexaPDF version 0.25.0)>>
75
+ <</Producer(HexaPDF version 0.26.0)>>
76
76
  endobj
77
77
  2 0 obj
78
78
  <</Length 10>>stream
@@ -206,7 +206,7 @@ describe HexaPDF::Writer do
206
206
  <</Type/Page/MediaBox[0 0 595 842]/Parent 2 0 R/Resources<<>>>>
207
207
  endobj
208
208
  5 0 obj
209
- <</Producer(HexaPDF version 0.25.0)>>
209
+ <</Producer(HexaPDF version 0.26.0)>>
210
210
  endobj
211
211
  4 0 obj
212
212
  <</Root 1 0 R/Info 5 0 R/Size 6/Type/XRef/W[1 1 2]/Index[0 6]/Filter/FlateDecode/DecodeParms<</Columns 4/Predictor 12>>/Length 33>>stream
@@ -54,6 +54,23 @@ describe HexaPDF::Type::Catalog do
54
54
  end
55
55
  end
56
56
 
57
+ describe "page_labels" do
58
+ it "returns an existing page labels number tree" do
59
+ @catalog[:PageLabels] = {Nums: []}
60
+ assert_equal({Nums: []}, @catalog.page_labels.value)
61
+ end
62
+
63
+ it "returns an existing page labels number tree even if create: true" do
64
+ obj = @catalog[:PageLabels] = {Nums: []}
65
+ assert_same(obj, @catalog.page_labels(create: true).value)
66
+ end
67
+
68
+ it "creates a new page labels number tree if create: true" do
69
+ tree = @catalog.page_labels(create: true)
70
+ assert_kind_of(HexaPDF::NumberTreeNode, tree)
71
+ end
72
+ end
73
+
57
74
  describe "validation" do
58
75
  it "creates the page tree if necessary" do
59
76
  refute(@catalog.validate(auto_correct: false))
@@ -37,6 +37,7 @@ describe HexaPDF::Type::FileSpecification do
37
37
  it "only sets /UF and /F, deleting /Mac, /Unix, /DOS entries if they exist" do
38
38
  @obj[:Unix] = @obj[:Mac] = @obj[:DOS] = 'a'
39
39
  @obj.path = 'file/test'
40
+ refute_same(@obj.value[:UF], @obj.value[:F])
40
41
  assert_equal('file/test', @obj[:UF])
41
42
  assert_equal('file/test', @obj[:F])
42
43
  refute(@obj.key?(:Unix))
@@ -327,6 +327,14 @@ describe HexaPDF::Type::Page do
327
327
  end
328
328
  end
329
329
 
330
+ describe "label" do
331
+ it "returns the label for the page" do
332
+ 5.times { @doc.pages.add }
333
+ @doc.pages.add_labelling_range(0, numbering_style: :uppercase_letters)
334
+ assert_equal(%w[A B C D E], @doc.pages.each.map(&:label))
335
+ end
336
+ end
337
+
330
338
  it "returns all ancestor page tree nodes of a page" do
331
339
  root = @doc.add({Type: :Pages})
332
340
  kid = @doc.add({Type: :Pages, Parent: root})
@@ -0,0 +1,118 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ require 'test_helper'
4
+ require 'hexapdf/document'
5
+ require 'hexapdf/type/page_label'
6
+
7
+ describe HexaPDF::Type::PageLabel do
8
+ before do
9
+ @doc = HexaPDF::Document.new
10
+ @page_label = @doc.wrap({Type: :PageLabel})
11
+ end
12
+
13
+ describe "construct_label" do
14
+ it "returns an empty label if nothing is set" do
15
+ assert_equal('', @page_label.construct_label(0))
16
+ end
17
+
18
+ it "returns the prefix if no numbering style is set" do
19
+ @page_label.prefix('hello')
20
+ assert_equal('hello', @page_label.construct_label(0))
21
+ end
22
+
23
+ it "works for decimal numbers" do
24
+ @page_label.numbering_style(:decimal)
25
+ assert_equal("10", @page_label.construct_label(9))
26
+ end
27
+
28
+ it "works for uppercase letters" do
29
+ @page_label.numbering_style(:uppercase_letters)
30
+ assert_equal("J", @page_label.construct_label(9))
31
+ assert_equal("AJ", @page_label.construct_label(35))
32
+ end
33
+
34
+ it "works for lowercase letters" do
35
+ @page_label.numbering_style(:lowercase_letters)
36
+ assert_equal("a", @page_label.construct_label(0))
37
+ assert_equal("aa", @page_label.construct_label(26))
38
+ end
39
+
40
+ it "works for uppercase roman numerals" do
41
+ @page_label.numbering_style(:uppercase_roman)
42
+ assert_equal("X", @page_label.construct_label(9))
43
+ end
44
+
45
+ it "works for lowercase roman numerals" do
46
+ @page_label.numbering_style(:lowercase_roman)
47
+ assert_equal("i", @page_label.construct_label(0))
48
+ assert_equal("iv", @page_label.construct_label(3))
49
+ end
50
+
51
+ it "combines the prefix with the numeric portion" do
52
+ @page_label.prefix('hello-')
53
+ @page_label.numbering_style(:decimal)
54
+ assert_equal('hello-1', @page_label.construct_label(0))
55
+ assert_equal('hello-101', @page_label.construct_label(100))
56
+ end
57
+ end
58
+
59
+ describe "numbering_style" do
60
+ it "returns the set numbering style" do
61
+ assert_equal(:none, @page_label.numbering_style)
62
+ @page_label[:S] = :D
63
+ assert_equal(:decimal, @page_label.numbering_style)
64
+ end
65
+
66
+ it "sets the numbering style to the given value" do
67
+ @page_label.numbering_style(:decimal)
68
+ assert_equal(:D, @page_label[:S])
69
+ @page_label.numbering_style(:none)
70
+ assert_nil(@page_label[:S])
71
+ end
72
+
73
+ it "returns :none for an unknown numbering style" do
74
+ @page_label[:S] = :d
75
+ assert_equal(:none, @page_label.numbering_style)
76
+ end
77
+
78
+ it "fails if the given value is not mapped to a numbering_style" do
79
+ assert_raises(ArgumentError) { @page_label.numbering_style("Nomad") }
80
+ assert_raises(ArgumentError) { @page_label.numbering_style(:unknown) }
81
+ end
82
+ end
83
+
84
+ describe "prefix" do
85
+ it "returns the set prefix" do
86
+ assert_nil(@page_label.prefix)
87
+ @page_label[:P] = 'Prefix'
88
+ assert_equal('Prefix', @page_label.prefix)
89
+ end
90
+
91
+ it "sets the prefix to the given value" do
92
+ @page_label.prefix('Hallo')
93
+ assert_equal('Hallo', @page_label[:P])
94
+ end
95
+ end
96
+
97
+ describe "start_number" do
98
+ it "returns the set start number" do
99
+ assert_equal(1, @page_label.start_number)
100
+ @page_label[:St] = 5
101
+ assert_equal(5, @page_label.start_number)
102
+ end
103
+
104
+ it "set the start number to the given value" do
105
+ @page_label.start_number(5)
106
+ assert_equal(5, @page_label[:St])
107
+ end
108
+
109
+ it "fails if the provided value is not an integer" do
110
+ assert_raises(ArgumentError) { @page_label.start_number("6") }
111
+ end
112
+
113
+ it "fails if the value is lower than 1" do
114
+ assert_raises(ArgumentError) { @page_label.start_number("-1") }
115
+ assert_raises(ArgumentError) { @page_label.start_number("0") }
116
+ end
117
+ end
118
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: hexapdf
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.25.0
4
+ version: 0.26.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Thomas Leitner
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2022-10-02 00:00:00.000000000 Z
11
+ date: 2022-10-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: cmdparse
@@ -420,11 +420,13 @@ files:
420
420
  - lib/hexapdf/type/icon_fit.rb
421
421
  - lib/hexapdf/type/image.rb
422
422
  - lib/hexapdf/type/info.rb
423
+ - lib/hexapdf/type/mark_information.rb
423
424
  - lib/hexapdf/type/names.rb
424
425
  - lib/hexapdf/type/object_stream.rb
425
426
  - lib/hexapdf/type/outline.rb
426
427
  - lib/hexapdf/type/outline_item.rb
427
428
  - lib/hexapdf/type/page.rb
429
+ - lib/hexapdf/type/page_label.rb
428
430
  - lib/hexapdf/type/page_tree_node.rb
429
431
  - lib/hexapdf/type/resources.rb
430
432
  - lib/hexapdf/type/signature.rb
@@ -670,6 +672,7 @@ files:
670
672
  - test/hexapdf/type/test_outline.rb
671
673
  - test/hexapdf/type/test_outline_item.rb
672
674
  - test/hexapdf/type/test_page.rb
675
+ - test/hexapdf/type/test_page_label.rb
673
676
  - test/hexapdf/type/test_page_tree_node.rb
674
677
  - test/hexapdf/type/test_resources.rb
675
678
  - test/hexapdf/type/test_signature.rb
@@ -703,7 +706,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
703
706
  - !ruby/object:Gem::Version
704
707
  version: '0'
705
708
  requirements: []
706
- rubygems_version: 3.3.3
709
+ rubygems_version: 3.2.32
707
710
  signing_key:
708
711
  specification_version: 4
709
712
  summary: HexaPDF - A Versatile PDF Creation and Manipulation Library For Ruby