hexapdf 0.33.0 → 0.34.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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +42 -1
  3. data/examples/026-optional_content.rb +55 -0
  4. data/examples/027-composer_optional_content.rb +83 -0
  5. data/lib/hexapdf/cli/command.rb +7 -1
  6. data/lib/hexapdf/cli/fonts.rb +1 -1
  7. data/lib/hexapdf/cli/inspect.rb +2 -4
  8. data/lib/hexapdf/composer.rb +2 -1
  9. data/lib/hexapdf/configuration.rb +21 -1
  10. data/lib/hexapdf/content/canvas.rb +52 -0
  11. data/lib/hexapdf/content/operator.rb +2 -0
  12. data/lib/hexapdf/dictionary.rb +1 -0
  13. data/lib/hexapdf/dictionary_fields.rb +1 -2
  14. data/lib/hexapdf/digital_signature/verification_result.rb +1 -2
  15. data/lib/hexapdf/document/layout.rb +3 -0
  16. data/lib/hexapdf/document/pages.rb +1 -1
  17. data/lib/hexapdf/document.rb +7 -0
  18. data/lib/hexapdf/encryption/ruby_aes.rb +10 -20
  19. data/lib/hexapdf/layout/box.rb +23 -3
  20. data/lib/hexapdf/layout/column_box.rb +2 -1
  21. data/lib/hexapdf/layout/frame.rb +23 -6
  22. data/lib/hexapdf/layout/inline_box.rb +20 -9
  23. data/lib/hexapdf/layout/list_box.rb +34 -20
  24. data/lib/hexapdf/layout/page_style.rb +2 -1
  25. data/lib/hexapdf/layout/style.rb +46 -6
  26. data/lib/hexapdf/layout/table_box.rb +9 -7
  27. data/lib/hexapdf/layout/text_box.rb +9 -2
  28. data/lib/hexapdf/layout/text_fragment.rb +28 -2
  29. data/lib/hexapdf/layout/text_layouter.rb +21 -5
  30. data/lib/hexapdf/stream.rb +1 -2
  31. data/lib/hexapdf/type/actions/set_ocg_state.rb +86 -0
  32. data/lib/hexapdf/type/actions.rb +1 -0
  33. data/lib/hexapdf/type/annotations/text.rb +1 -2
  34. data/lib/hexapdf/type/catalog.rb +10 -1
  35. data/lib/hexapdf/type/cid_font.rb +15 -1
  36. data/lib/hexapdf/type/form.rb +75 -5
  37. data/lib/hexapdf/type/optional_content_configuration.rb +170 -0
  38. data/lib/hexapdf/type/optional_content_group.rb +370 -0
  39. data/lib/hexapdf/type/optional_content_membership.rb +63 -0
  40. data/lib/hexapdf/type/optional_content_properties.rb +158 -0
  41. data/lib/hexapdf/type/page.rb +27 -11
  42. data/lib/hexapdf/type/page_label.rb +4 -8
  43. data/lib/hexapdf/type.rb +4 -0
  44. data/lib/hexapdf/utils/pdf_doc_encoding.rb +0 -1
  45. data/lib/hexapdf/version.rb +1 -1
  46. data/test/hexapdf/content/test_canvas.rb +49 -0
  47. data/test/hexapdf/document/test_layout.rb +7 -2
  48. data/test/hexapdf/document/test_pages.rb +6 -6
  49. data/test/hexapdf/layout/test_box.rb +13 -4
  50. data/test/hexapdf/layout/test_frame.rb +13 -1
  51. data/test/hexapdf/layout/test_inline_box.rb +17 -8
  52. data/test/hexapdf/layout/test_list_box.rb +48 -31
  53. data/test/hexapdf/layout/test_style.rb +10 -0
  54. data/test/hexapdf/layout/test_table_box.rb +32 -26
  55. data/test/hexapdf/layout/test_text_box.rb +8 -0
  56. data/test/hexapdf/layout/test_text_fragment.rb +33 -0
  57. data/test/hexapdf/layout/test_text_layouter.rb +32 -5
  58. data/test/hexapdf/test_composer.rb +10 -0
  59. data/test/hexapdf/test_dictionary.rb +10 -0
  60. data/test/hexapdf/test_document.rb +4 -0
  61. data/test/hexapdf/test_writer.rb +3 -3
  62. data/test/hexapdf/type/actions/test_set_ocg_state.rb +40 -0
  63. data/test/hexapdf/type/test_catalog.rb +11 -0
  64. data/test/hexapdf/type/test_form.rb +119 -0
  65. data/test/hexapdf/type/test_optional_content_configuration.rb +112 -0
  66. data/test/hexapdf/type/test_optional_content_group.rb +158 -0
  67. data/test/hexapdf/type/test_optional_content_properties.rb +109 -0
  68. data/test/hexapdf/type/test_page.rb +2 -2
  69. metadata +14 -3
@@ -0,0 +1,86 @@
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-2023 Thomas Leitner
8
+ #
9
+ # HexaPDF is free software: you can redistribute it and/or modify it
10
+ # under the terms of the GNU Affero General Public License version 3 as
11
+ # published by the Free Software Foundation with the addition of the
12
+ # following permission added to Section 15 as permitted in Section 7(a):
13
+ # FOR ANY PART OF THE COVERED WORK IN WHICH THE COPYRIGHT IS OWNED BY
14
+ # THOMAS LEITNER, THOMAS LEITNER DISCLAIMS THE WARRANTY OF NON
15
+ # INFRINGEMENT OF THIRD PARTY RIGHTS.
16
+ #
17
+ # HexaPDF is distributed in the hope that it will be useful, but WITHOUT
18
+ # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
19
+ # FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public
20
+ # License for more details.
21
+ #
22
+ # You should have received a copy of the GNU Affero General Public License
23
+ # along with HexaPDF. If not, see <http://www.gnu.org/licenses/>.
24
+ #
25
+ # The interactive user interfaces in modified source and object code
26
+ # versions of HexaPDF must display Appropriate Legal Notices, as required
27
+ # under Section 5 of the GNU Affero General Public License version 3.
28
+ #
29
+ # In accordance with Section 7(b) of the GNU Affero General Public
30
+ # License, a covered work must retain the producer line in every PDF that
31
+ # is created or manipulated using HexaPDF.
32
+ #
33
+ # If the GNU Affero General Public License doesn't fit your need,
34
+ # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
+ #++
36
+
37
+ require 'hexapdf/type/action'
38
+
39
+ module HexaPDF
40
+ module Type
41
+ module Actions
42
+
43
+ # A Set-OCG-state action changes the state of one or more optional content groups.
44
+ #
45
+ # See: PDF2.0 s12.6.4.13, HexaPDF::Type::OptionalContentGroup
46
+ class SetOCGState < Action
47
+
48
+ define_field :S, type: Symbol, required: true, default: :SetOCGState
49
+ define_field :State, type: PDFArray, required: true, default: []
50
+ define_field :PreserveRB, type: Boolean, default: true
51
+
52
+ STATE_TYPE_MAPPING = {on: :ON, ON: :ON, off: :OFF, OFF: :OFF, # :nodoc:
53
+ toggle: :Toggle, Toggle: :Toggle}
54
+
55
+ # Adds a state changing sequence to the /State array.
56
+ #
57
+ # The +type+ argument specifies how the state of the given optional content groups should be
58
+ # changed.
59
+ #
60
+ # +type+:: The type of sequence to add, either :on/:ON (for turning the OCGs on) , :off/:OFF
61
+ # (for turning the OCGs off), or :toggle/:Toggle (for toggling the state of the
62
+ # OCGs).
63
+ #
64
+ # +ocgs+:: A single optional content group or an array of optional content groups to which
65
+ # the state change defined with +type+ should be applied. The OCGs can be specified
66
+ # via their dictionary or by name which uses the first found OCG with that name.
67
+ def add_state_change(type, ocgs)
68
+ type = STATE_TYPE_MAPPING.fetch(type) do
69
+ raise ArgumentError, "Invalid type #{type} specified, should be one of :on, :off or :toggle"
70
+ end
71
+ state = self[:State]
72
+ state << type
73
+ Array(ocgs).each do |ocg|
74
+ if (ocg_name = ocg).kind_of?(String)
75
+ ocg = document.optional_content.ocg(ocg_name, create: false)
76
+ raise HexaPDF::Error, "Invalid OCG named '#{ocg_name}' specified" unless ocg
77
+ end
78
+ state << ocg
79
+ end
80
+ end
81
+
82
+ end
83
+
84
+ end
85
+ end
86
+ end
@@ -48,6 +48,7 @@ module HexaPDF
48
48
  autoload(:GoToR, 'hexapdf/type/actions/go_to_r')
49
49
  autoload(:Launch, 'hexapdf/type/actions/launch')
50
50
  autoload(:URI, 'hexapdf/type/actions/uri')
51
+ autoload(:SetOCGState, 'hexapdf/type/actions/set_ocg_state')
51
52
 
52
53
  end
53
54
 
@@ -55,8 +55,7 @@ module HexaPDF
55
55
 
56
56
  private
57
57
 
58
- # :nodoc:
59
- STATE_TO_STATE_MODEL = {
58
+ STATE_TO_STATE_MODEL = { # :nodoc:
60
59
  "Marked" => "Marked", "Unmarked" => "Marked",
61
60
  "Accepted" => "Review", "Rejected" => "Review", "Cancelled" => "Review",
62
61
  "Completed" => "Review", "None" => "Review"
@@ -78,7 +78,7 @@ module HexaPDF
78
78
  define_field :SpiderInfo, type: Dictionary, version: '1.3'
79
79
  define_field :OutputIntents, type: PDFArray, version: '1.4'
80
80
  define_field :PieceInfo, type: Dictionary, version: '1.4'
81
- define_field :OCProperties, type: Dictionary, version: '1.5'
81
+ define_field :OCProperties, type: :XXOCProperties, version: '1.5'
82
82
  define_field :Perms, type: Dictionary, version: '1.5'
83
83
  define_field :Legal, type: Dictionary, version: '1.5'
84
84
  define_field :Requirements, type: PDFArray, version: '1.7'
@@ -112,6 +112,15 @@ module HexaPDF
112
112
  self[:Outlines] ||= document.add({}, type: :Outlines)
113
113
  end
114
114
 
115
+ # Returns the optional content properties dictionary, creating it if needed.
116
+ #
117
+ # This is the main entry point for working with optional content, a.k.a. layers.
118
+ #
119
+ # See: OptionalContentProperties
120
+ def optional_content
121
+ self[:OCProperties] ||= document.add({OCGs: [], D: {Creator: 'HexaPDF'}}, type: :XXOCProperties)
122
+ end
123
+
115
124
  # Returns the main AcroForm object.
116
125
  #
117
126
  # * If an AcroForm object exists, the +create+ argument is not used.
@@ -45,10 +45,24 @@ module HexaPDF
45
45
  # See: PDF2.0 s9.7.4
46
46
  class CIDFont < Font
47
47
 
48
+ # Describes the CIDSystemInfo dictionary specifying the character collection assumed by the
49
+ # CIDFont.
50
+ #
51
+ # See: PDF2.0 s9.7.3
52
+ class CIDSystemInfo < Dictionary
53
+
54
+ define_type :XXCIDSystemInfo
55
+
56
+ define_field :Registry, type: String, required: true
57
+ define_field :Ordering, type: String, required: true
58
+ define_field :Supplement, type: Integer, required: true
59
+
60
+ end
61
+
48
62
  DEFAULT_WIDTH = 1000 # :nodoc:
49
63
 
50
64
  define_field :BaseFont, type: Symbol, required: true
51
- define_field :CIDSystemInfo, type: Dictionary, required: true
65
+ define_field :CIDSystemInfo, type: :XXCIDSystemInfo, required: true
52
66
  define_field :FontDescriptor, type: :FontDescriptor, indirect: true, required: true
53
67
  define_field :DW, type: Integer, default: DEFAULT_WIDTH
54
68
  define_field :W, type: PDFArray
@@ -34,6 +34,7 @@
34
34
  # commercial licenses are available at <https://gettalong.at/hexapdf/>.
35
35
  #++
36
36
 
37
+ require 'stringio'
37
38
  require 'hexapdf/stream'
38
39
  require 'hexapdf/content'
39
40
 
@@ -45,6 +46,32 @@ module HexaPDF
45
46
  # See: PDF2.0 s8.10
46
47
  class Form < Stream
47
48
 
49
+ # Represents a group attribute dictionary.
50
+ #
51
+ # See: PDF2.0 s8.10.3
52
+ class Group < Dictionary
53
+
54
+ define_type :Group
55
+
56
+ define_field :Type, type: Symbol, default: type
57
+ define_field :S, type: Symbol, required: true
58
+
59
+ end
60
+
61
+ # Represents a reference dictionary which allows an XObject to refer to content in an embedded
62
+ # or linked PDF document.
63
+ #
64
+ # See: PDF2.0 s8.10.4
65
+ class Reference < Dictionary
66
+
67
+ define_type :XXReference
68
+
69
+ define_field :F, type: :Filespec, required: true
70
+ define_field :Page, type: [Integer, String], required: true
71
+ define_field :ID, type: PDFArray
72
+
73
+ end
74
+
48
75
  define_type :XObject
49
76
 
50
77
  define_field :Type, type: Symbol, default: type
@@ -53,8 +80,8 @@ module HexaPDF
53
80
  define_field :BBox, type: Rectangle, required: true
54
81
  define_field :Matrix, type: PDFArray, default: [1, 0, 0, 1, 0, 0]
55
82
  define_field :Resources, type: :XXResources, version: '1.2'
56
- define_field :Group, type: Dictionary, version: '1.4'
57
- define_field :Ref, type: Dictionary, version: '1.4'
83
+ define_field :Group, type: :Group, version: '1.4'
84
+ define_field :Ref, type: :XXReference, version: '1.4'
58
85
  define_field :Metadata, type: Stream, version: '1.4'
59
86
  define_field :PieceInfo, type: Dictionary, version: '1.3'
60
87
  define_field :LastModified, type: PDFDate, version: '1.3'
@@ -115,14 +142,15 @@ module HexaPDF
115
142
  #
116
143
  # See: HexaPDF::Content::Processor
117
144
  def process_contents(processor, original_resources: nil)
118
- processor.resources = if self[:Resources]
119
- self[:Resources]
145
+ form = referenced_content || self
146
+ processor.resources = if form[:Resources]
147
+ form[:Resources]
120
148
  elsif original_resources
121
149
  original_resources
122
150
  else
123
151
  document.wrap({}, type: :XXResources)
124
152
  end
125
- Content::Parser.parse(contents, processor)
153
+ Content::Parser.parse(form.contents, processor)
126
154
  end
127
155
 
128
156
  # Returns the canvas for the form XObject.
@@ -152,6 +180,48 @@ module HexaPDF
152
180
  end
153
181
  end
154
182
 
183
+ # Returns +true+ if the Form XObject is a reference XObject.
184
+ def reference_xobject?
185
+ !self[:Ref].nil?
186
+ end
187
+
188
+ # Returns the referenced page as Form XObject, if this Form XObject is a Reference XObject and
189
+ # the referenced page is found. Otherwise returns +nil+.
190
+ def referenced_content
191
+ return unless (ref = self[:Ref])
192
+
193
+ doc = if ref[:F].embedded_file?
194
+ HexaPDF::Document.new(io: StringIO.new(ref[:F].embedded_file_stream.stream))
195
+ elsif File.exist?(ref[:F].path)
196
+ HexaPDF::Document.open(ref[:F].path)
197
+ end
198
+ return unless doc
199
+
200
+ page = ref[:Page]
201
+ if page.kind_of?(Integer)
202
+ page = doc.pages[page]
203
+ else
204
+ labels = []
205
+ doc.pages.each_labelling_range do |first_index, count, label|
206
+ count.times {|i| labels << label.construct_label(i) }
207
+ end
208
+ index = labels.index(page)
209
+ page = index && doc.pages[index]
210
+ end
211
+ return unless page
212
+
213
+ # See PDF2.0 s8.10.4.3
214
+ print_annots = page.each_annotation.select {|annot| annot.flagged?(:print) }
215
+ page.flatten_annotations(print_annots) unless print_annots.empty?
216
+
217
+ obj = page.to_form_xobject
218
+ obj[:BBox] = self[:BBox].dup
219
+ obj[:Matrix] = self[:Matrix].dup
220
+ obj
221
+ rescue
222
+ nil
223
+ end
224
+
155
225
  end
156
226
 
157
227
  end
@@ -0,0 +1,170 @@
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-2023 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 an optional content configuration dictionary.
43
+ #
44
+ # This dictionary is used for the /D and /Configs entries in the optional content properties
45
+ # dictionary. It configures the states of the OCGs as well as defines how those states may be
46
+ # changed by a PDF processor.
47
+ #
48
+ # See: PDF2.0 s8.11.4.3
49
+ class OptionalContentConfiguration < Dictionary
50
+
51
+ # Represents an optional content usage application dictionary.
52
+ #
53
+ # This dictionary is used for the elements in the /AS array of an optional content
54
+ # configuration dictionary. It specifies how a PDF processor should use the usage entries of
55
+ # OCGs to automatically change their state based on external factors (like magnifacation
56
+ # factor or language).
57
+ #
58
+ # See: PDF2.0 s8.11.4.4
59
+ class UsageApplication < Dictionary
60
+ define_type :XXOCUsageApplication
61
+ define_field :Event, type: Symbol, required: true, allowed_values: [:View, :Print, :Export]
62
+ define_field :OCGs, type: PDFArray, default: []
63
+ define_field :Category, type: PDFArray, required: true
64
+ end
65
+
66
+ define_type :XXOCConfiguration
67
+
68
+ define_field :Name, type: String
69
+ define_field :Creator, type: String
70
+ define_field :BaseState, type: Symbol, default: :ON, allowed_values: [:ON, :OFF, :Unchanged]
71
+ define_field :ON, type: PDFArray
72
+ define_field :OFF, type: PDFArray
73
+ define_field :Intent, type: [Symbol, PDFArray], default: :View
74
+ define_field :AS, type: PDFArray
75
+ define_field :Order, type: PDFArray
76
+ define_field :ListMode, type: Symbol, default: :AllPages,
77
+ allowed_values: [:AllPages, :VisiblePages]
78
+ define_field :RBGroups, type: PDFArray
79
+ define_field :Locked, type: PDFArray, default: []
80
+
81
+ # :call-seq:
82
+ # configuration.ocg_state(ocg) -> state
83
+ # configuration.ocg_state(ocg, state) -> state
84
+ #
85
+ # Returns the state (+:on+, +:off+ or +nil+) of the optional content group if the +state+
86
+ # argument is not given. Otherwise sets the state of the OCG to the given state value
87
+ # (+:on+/+:ON+ or +:off+/+:OFF+).
88
+ #
89
+ # The value +nil+ is only returned if the state is not defined by the configuration dictionary
90
+ # (which may only be the case if the configuration dictionary is not the default configuration
91
+ # dictionary).
92
+ def ocg_state(ocg, state = nil)
93
+ if state.nil?
94
+ case self[:BaseState]
95
+ when :ON then self[:OFF]&.include?(ocg) ? :off : :on
96
+ when :OFF then self[:ON]&.include?(ocg) ? :on : :off
97
+ else self[:OFF]&.include?(ocg) ? :off : (self[:ON]&.include?(ocg) ? :on : nil)
98
+ end
99
+ elsif state&.downcase == :on
100
+ (self[:ON] ||= []) << ocg unless self[:ON]&.include?(ocg)
101
+ self[:OFF].delete(ocg) if key?(:OFF)
102
+ elsif state&.downcase == :off
103
+ (self[:OFF] ||= []) << ocg unless self[:OFF]&.include?(ocg)
104
+ self[:ON].delete(ocg) if key?(:ON)
105
+ else
106
+ raise ArgumentError, "Invalid value #{state.inspect} for state argument"
107
+ end
108
+ end
109
+
110
+ # Returns +true+ if the given optional content group is on.
111
+ def ocg_on?(ocg)
112
+ ocg_state(ocg) == :on
113
+ end
114
+
115
+ # Makes the given optional content group visible in an interactive PDF processor's user
116
+ # interface.
117
+ #
118
+ # The OCG is always added to the end of the specified +path+ or, if +path+ is not specified,
119
+ # the top level.
120
+ #
121
+ # The optional argument +path+ specifies the strings or OCGs under which the given OCG should
122
+ # hierarchically be nested. A string is used as a non-selectable label, an OCG reflects an
123
+ # actual nesting of the involved OCGs.
124
+ #
125
+ # Examples:
126
+ #
127
+ # configuration.add_ocg_to_ui(ocg) # Add the OCG as top-level item
128
+ # configuration.add_ocg_to_ui(ocg, path: 'Debug') # Add the OCG under the label 'Debug'
129
+ # # Add the OCG under the label 'Page1' which is under the label 'Debug'
130
+ # configuration.add_ocg_to_ui(ocg, path: ['Debug', 'Page1'])
131
+ # configuration.add_ocg_to_ui(ocg, path: other_ocg) # Add the OCG under the other OCG
132
+ def add_ocg_to_ui(ocg, path: nil)
133
+ array = self[:Order] ||= []
134
+ path = Array(path)
135
+ until path.empty?
136
+ item = path.shift
137
+ index = array.index do |entry|
138
+ if (entry.kind_of?(Array) || entry.kind_of?(PDFArray)) && item.kind_of?(String)
139
+ entry.first == item
140
+ else
141
+ entry == item
142
+ end
143
+ end
144
+
145
+ if item.kind_of?(String)
146
+ unless index
147
+ array << [item]
148
+ index = -1
149
+ end
150
+ array = array[index]
151
+ else
152
+ unless index
153
+ array << item << []
154
+ index = -2
155
+ end
156
+ if array[index + 1].kind_of?(Array) || array[index + 1].kind_of?(PDFArray)
157
+ array = array[index + 1]
158
+ else
159
+ array.insert(index + 1, [])
160
+ array = array[index + 1]
161
+ end
162
+ end
163
+ end
164
+ array << ocg
165
+ end
166
+
167
+ end
168
+
169
+ end
170
+ end