hexapdf 0.25.0 → 0.26.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 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