hexapdf 0.47.0 → 1.0.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -16
  3. data/lib/hexapdf/cli.rb +14 -1
  4. data/lib/hexapdf/composer.rb +7 -0
  5. data/lib/hexapdf/configuration.rb +2 -0
  6. data/lib/hexapdf/content/parser.rb +3 -1
  7. data/lib/hexapdf/digital_signature/cms_handler.rb +13 -0
  8. data/lib/hexapdf/digital_signature/signature.rb +1 -1
  9. data/lib/hexapdf/digital_signature/signing/default_handler.rb +1 -0
  10. data/lib/hexapdf/document.rb +14 -3
  11. data/lib/hexapdf/font/cmap/writer.rb +58 -4
  12. data/lib/hexapdf/font/cmap.rb +7 -0
  13. data/lib/hexapdf/font/true_type_wrapper.rb +41 -16
  14. data/lib/hexapdf/layout/text_fragment.rb +2 -1
  15. data/lib/hexapdf/object.rb +1 -1
  16. data/lib/hexapdf/parser.rb +6 -2
  17. data/lib/hexapdf/reference.rb +1 -1
  18. data/lib/hexapdf/task/merge_acro_form.rb +164 -0
  19. data/lib/hexapdf/task.rb +1 -0
  20. data/lib/hexapdf/tokenizer.rb +2 -0
  21. data/lib/hexapdf/type/acro_form/form.rb +14 -27
  22. data/lib/hexapdf/type/acro_form/signature_field.rb +16 -6
  23. data/lib/hexapdf/type/acro_form/variable_text_field.rb +1 -1
  24. data/lib/hexapdf/type/actions/go_to.rb +1 -0
  25. data/lib/hexapdf/type/actions/go_to_r.rb +1 -0
  26. data/lib/hexapdf/type/actions/launch.rb +5 -1
  27. data/lib/hexapdf/type/annotation.rb +6 -1
  28. data/lib/hexapdf/type/annotations/markup_annotation.rb +14 -1
  29. data/lib/hexapdf/type/catalog.rb +3 -0
  30. data/lib/hexapdf/type/cid_font.rb +4 -1
  31. data/lib/hexapdf/type/file_specification.rb +17 -14
  32. data/lib/hexapdf/type/font_descriptor.rb +4 -3
  33. data/lib/hexapdf/type/font_simple.rb +3 -1
  34. data/lib/hexapdf/type/font_true_type.rb +2 -0
  35. data/lib/hexapdf/type/font_type0.rb +1 -1
  36. data/lib/hexapdf/type/font_type1.rb +7 -0
  37. data/lib/hexapdf/type/font_type3.rb +0 -1
  38. data/lib/hexapdf/type/form.rb +5 -2
  39. data/lib/hexapdf/type/graphics_state_parameter.rb +7 -4
  40. data/lib/hexapdf/type/image.rb +8 -4
  41. data/lib/hexapdf/type/info.rb +2 -2
  42. data/lib/hexapdf/type/mark_information.rb +2 -2
  43. data/lib/hexapdf/type/optional_content_configuration.rb +1 -1
  44. data/lib/hexapdf/type/optional_content_membership.rb +1 -1
  45. data/lib/hexapdf/type/page.rb +5 -3
  46. data/lib/hexapdf/type/resources.rb +6 -6
  47. data/lib/hexapdf/type/viewer_preferences.rb +4 -3
  48. data/lib/hexapdf/utils/sorted_tree_node.rb +12 -2
  49. data/lib/hexapdf/version.rb +1 -1
  50. data/lib/hexapdf/writer.rb +1 -0
  51. data/lib/hexapdf/xref_section.rb +20 -4
  52. data/test/hexapdf/common_tokenizer_tests.rb +5 -0
  53. data/test/hexapdf/digital_signature/signing/test_default_handler.rb +6 -0
  54. data/test/hexapdf/digital_signature/test_cms_handler.rb +12 -7
  55. data/test/hexapdf/digital_signature/test_signature.rb +7 -0
  56. data/test/hexapdf/digital_signature/test_signatures.rb +8 -3
  57. data/test/hexapdf/font/cmap/test_writer.rb +73 -16
  58. data/test/hexapdf/font/test_true_type_wrapper.rb +17 -3
  59. data/test/hexapdf/layout/test_list_box.rb +7 -7
  60. data/test/hexapdf/layout/test_text_fragment.rb +3 -3
  61. data/test/hexapdf/layout/test_text_layouter.rb +4 -2
  62. data/test/hexapdf/task/test_merge_acro_form.rb +104 -0
  63. data/test/hexapdf/test_composer.rb +8 -0
  64. data/test/hexapdf/test_document.rb +9 -0
  65. data/test/hexapdf/test_parser.rb +23 -6
  66. data/test/hexapdf/test_writer.rb +10 -5
  67. data/test/hexapdf/test_xref_section.rb +15 -0
  68. data/test/hexapdf/type/acro_form/test_appearance_generator.rb +18 -18
  69. data/test/hexapdf/type/acro_form/test_form.rb +7 -3
  70. data/test/hexapdf/type/actions/test_launch.rb +6 -2
  71. data/test/hexapdf/type/test_font_type1.rb +5 -0
  72. data/test/hexapdf/type/test_form.rb +1 -1
  73. data/test/hexapdf/type/test_page.rb +7 -1
  74. data/test/hexapdf/utils/test_sorted_tree_node.rb +7 -6
  75. metadata +4 -2
@@ -61,10 +61,10 @@ module HexaPDF
61
61
  define_field :ColorSpace, type: [Symbol, PDFArray]
62
62
  define_field :BitsPerComponent, type: Integer
63
63
  define_field :Intent, type: Symbol, version: '1.1',
64
- allowed_values: [HexaPDF::Content::RenderingIntent::ABSOLUTE_COLORIMETRIC,
65
- HexaPDF::Content::RenderingIntent::RELATIVE_COLORIMETRIC,
66
- HexaPDF::Content::RenderingIntent::SATURATION,
67
- HexaPDF::Content::RenderingIntent::PERCEPTUAL]
64
+ allowed_values: [HexaPDF::Content::RenderingIntent::ABSOLUTE_COLORIMETRIC,
65
+ HexaPDF::Content::RenderingIntent::RELATIVE_COLORIMETRIC,
66
+ HexaPDF::Content::RenderingIntent::SATURATION,
67
+ HexaPDF::Content::RenderingIntent::PERCEPTUAL]
68
68
  define_field :ImageMask, type: Boolean, default: false
69
69
  define_field :Mask, type: [Stream, PDFArray], version: '1.3'
70
70
  define_field :Decode, type: PDFArray
@@ -72,11 +72,15 @@ module HexaPDF
72
72
  define_field :Alternates, type: PDFArray, version: '1.3'
73
73
  define_field :SMask, type: Stream, version: '1.4'
74
74
  define_field :SMaskInData, type: Integer, version: '1.5', allowed_values: [0, 1, 2]
75
+ define_field :Name, type: Symbol
75
76
  define_field :StructParent, type: Integer, version: '1.3'
76
77
  define_field :ID, type: PDFByteString, version: '1.3'
77
78
  define_field :OPI, type: Dictionary, version: '1.2'
78
79
  define_field :Metadata, type: Stream, version: '1.4'
79
80
  define_field :OC, type: Dictionary, version: '1.5'
81
+ define_field :AF, type: PDFArray, version: '2.0'
82
+ define_field :Measure, type: Dictionary, version: '2.0'
83
+ define_field :PtData, type: Dictionary, version: '2.0'
80
84
 
81
85
  # Returns the source path that was used when creating the image object.
82
86
  #
@@ -57,8 +57,8 @@ module HexaPDF
57
57
  define_field :Producer, type: String
58
58
  define_field :CreationDate, type: PDFDate
59
59
  define_field :ModDate, type: PDFDate
60
- define_field :Trapped, type: Symbol, version: '1.3',
61
- allowed_values: [:True, :False, :Unknown]
60
+ define_field :Trapped, type: Symbol, version: '1.3', default: :Unknown,
61
+ allowed_values: [:True, :False, :Unknown]
62
62
 
63
63
  # Info dictionaries must always be indirect.
64
64
  def must_be_indirect?
@@ -48,8 +48,8 @@ module HexaPDF
48
48
  define_type :XXMarkInformation
49
49
 
50
50
  define_field :Marked, type: Boolean, default: false
51
- define_field :UserProperties, type: Boolean, default: false
52
- define_field :Suspects, type: Boolean, default: false
51
+ define_field :UserProperties, type: Boolean, default: false, version: '1.6'
52
+ define_field :Suspects, type: Boolean, default: false, version: '1.6'
53
53
 
54
54
  end
55
55
 
@@ -76,7 +76,7 @@ module HexaPDF
76
76
  define_field :AS, type: PDFArray
77
77
  define_field :Order, type: PDFArray
78
78
  define_field :ListMode, type: Symbol, default: :AllPages,
79
- allowed_values: [:AllPages, :VisiblePages]
79
+ allowed_values: [:AllPages, :VisiblePages]
80
80
  define_field :RBGroups, type: PDFArray
81
81
  define_field :Locked, type: PDFArray, default: []
82
82
 
@@ -54,7 +54,7 @@ module HexaPDF
54
54
  define_field :Type, type: Symbol, required: true, default: type
55
55
  define_field :OCGs, type: [:OCG, PDFArray]
56
56
  define_field :P, type: Symbol, default: :AnyOn,
57
- allowed_values: [:AllOn, :AnyOn, :AnyOff, :AllOff]
57
+ allowed_values: [:AllOn, :AnyOn, :AnyOff, :AllOff]
58
58
  define_field :VE, type: PDFArray
59
59
 
60
60
  end
@@ -159,6 +159,9 @@ module HexaPDF
159
159
  define_field :PresSteps, type: Dictionary, version: '1.5'
160
160
  define_field :UserUnit, type: Numeric, version: '1.6'
161
161
  define_field :VP, type: PDFArray, version: '1.6'
162
+ define_field :AF, type: PDFArray, version: '2.0'
163
+ define_field :OutputIntents, type: PDFArray, version: '2.0'
164
+ define_field :DPart, type: Dictionary, version: '2.0'
162
165
 
163
166
  # Returns +true+ since page objects must always be indirect.
164
167
  def must_be_indirect?
@@ -358,7 +361,7 @@ module HexaPDF
358
361
  def contents
359
362
  Array(self[:Contents]).each_with_object("".b) do |content_stream, content|
360
363
  content << " " unless content.empty?
361
- content << content_stream.stream
364
+ content << content_stream.stream if content_stream.kind_of?(Stream)
362
365
  end
363
366
  end
364
367
 
@@ -380,8 +383,7 @@ module HexaPDF
380
383
  # Returns the, possibly inherited, resource dictionary which is automatically created if it
381
384
  # doesn't exist.
382
385
  def resources
383
- self[:Resources] ||= document.wrap({ProcSet: [:PDF, :Text, :ImageB, :ImageC, :ImageI]},
384
- type: :XXResources)
386
+ self[:Resources] ||= document.wrap({}, type: :XXResources)
385
387
  end
386
388
 
387
389
  # Processes the content streams associated with the page with the given processor object.
@@ -49,13 +49,13 @@ module HexaPDF
49
49
 
50
50
  define_type :XXResources
51
51
 
52
- define_field :ExtGState, type: Dictionary
52
+ define_field :ExtGState, type: Dictionary
53
53
  define_field :ColorSpace, type: Dictionary
54
- define_field :Pattern, type: Dictionary
55
- define_field :Shading, type: Dictionary, version: '1.3'
56
- define_field :XObject, type: Dictionary
57
- define_field :Font, type: Dictionary
58
- define_field :ProcSet, type: PDFArray
54
+ define_field :Pattern, type: Dictionary
55
+ define_field :Shading, type: Dictionary, version: '1.3'
56
+ define_field :XObject, type: Dictionary
57
+ define_field :Font, type: Dictionary
58
+ define_field :ProcSet, type: PDFArray
59
59
  define_field :Properties, type: Dictionary, version: '1.2'
60
60
 
61
61
  # Returns the color space stored under the given name.
@@ -56,19 +56,20 @@ module HexaPDF
56
56
  define_field :CenterWindow, type: Boolean, default: false
57
57
  define_field :DisplayDocTitle, type: Boolean, default: false, version: '1.4'
58
58
  define_field :NonFullScreenPageMode, type: Symbol, default: :UseNone,
59
- allowed_values: [:UseNone, :UseOutlines, :UseThumbs, :UseOC]
59
+ allowed_values: [:UseNone, :UseOutlines, :UseThumbs, :UseOC]
60
60
  define_field :Direction, type: Symbol, default: :L2R, version: '1.3',
61
- allowed_values: [:L2R, :R2L]
61
+ allowed_values: [:L2R, :R2L]
62
62
  define_field :ViewArea, type: Symbol, default: :CropBox, version: '1.4'
63
63
  define_field :ViewClip, type: Symbol, default: :CropBox, version: '1.4'
64
64
  define_field :PrintArea, type: Symbol, default: :CropBox, version: '1.4'
65
65
  define_field :PrintClip, type: Symbol, default: :CropBox, version: '1.4'
66
66
  define_field :PrintScaling, type: Symbol, default: :AppDefault, version: '1.6'
67
67
  define_field :Duplex, type: Symbol, version: '1.7',
68
- allowed_values: [:Simplex, :DuplexFlipShortEdge, :DuplexFlipLongEdge]
68
+ allowed_values: [:Simplex, :DuplexFlipShortEdge, :DuplexFlipLongEdge]
69
69
  define_field :PickTrayByPDFSize, type: Boolean, version: '1.7'
70
70
  define_field :PrintPageRange, type: PDFArray, version: '1.7'
71
71
  define_field :NumCopies, type: Integer, version: '1.7'
72
+ define_field :Enforce, type: PDFArray, version: '2.0'
72
73
 
73
74
  end
74
75
 
@@ -174,6 +174,7 @@ module HexaPDF
174
174
  elsif node.key?(:Kids)
175
175
  index = find_in_intermediate_node(node[:Kids], key)
176
176
  node = node[:Kids][index]
177
+ node = document.wrap(node, type: self.class) if node
177
178
  break unless node && key >= node[:Limits][0] && key <= node[:Limits][1]
178
179
  else
179
180
  break
@@ -194,7 +195,7 @@ module HexaPDF
194
195
  container_name = leaf_node_container_name
195
196
  stack = [self]
196
197
  until stack.empty?
197
- node = stack.pop
198
+ node = document.wrap(stack.pop, type: self.class)
198
199
  if node.key?(container_name)
199
200
  data = node[container_name]
200
201
  index = 0
@@ -217,7 +218,7 @@ module HexaPDF
217
218
  def path_to_key(node, key, stack)
218
219
  return unless node.key?(:Kids)
219
220
  index = find_in_intermediate_node(node[:Kids], key)
220
- stack << node[:Kids][index]
221
+ stack << document.wrap(node[:Kids][index], type: self.class)
221
222
  path_to_key(stack.last, key, stack)
222
223
  end
223
224
 
@@ -307,6 +308,15 @@ module HexaPDF
307
308
  super
308
309
  container_name = leaf_node_container_name
309
310
 
311
+ if key?(:Kids)
312
+ self[:Kids].each do |kid|
313
+ unless kid.indirect?
314
+ yield("Children of sorted tree nodes must be indirect", true)
315
+ document.add(kid)
316
+ end
317
+ end
318
+ end
319
+
310
320
  # All keys of the container must be lexically ordered strings and the container must be
311
321
  # correctly formatted
312
322
  if key?(container_name)
@@ -37,6 +37,6 @@
37
37
  module HexaPDF
38
38
 
39
39
  # The version of HexaPDF.
40
- VERSION = '0.47.0'
40
+ VERSION = '1.0.1'
41
41
 
42
42
  end
@@ -149,6 +149,7 @@ module HexaPDF
149
149
  obj_to_stm = object_streams.each_with_object({}) {|stm, m| m.update(stm.write_objects(rev)) }
150
150
 
151
151
  xref_section = XRefSection.new
152
+ xref_section.mark_as_initial_section! unless previous_xref_pos
152
153
  xref_section.add_free_entry(0, 65535) if previous_xref_pos.nil?
153
154
  rev.each do |obj|
154
155
  if obj.null?
@@ -111,6 +111,13 @@ module HexaPDF
111
111
  # used.
112
112
  private :'[]='
113
113
 
114
+ # Marks this XRefSection object as being the first cross-reference section in a PDF file.
115
+ #
116
+ # This has the consequence that only a single sub-section is created.
117
+ def mark_as_initial_section!
118
+ @initial_section = true
119
+ end
120
+
114
121
  # Adds an in-use entry to the cross-reference section.
115
122
  #
116
123
  # See: ::in_use_entry
@@ -147,15 +154,24 @@ module HexaPDF
147
154
  # If this section contains no objects, a single empty array is yielded (corresponding to a
148
155
  # subsection with zero elements).
149
156
  #
150
- # The subsections are dynamically generated based on the object numbers in this section.
157
+ # The subsections are dynamically generated based on the object numbers in this section. In case
158
+ # the section was marked as the initial section (see #mark_as_initial_section!) only a single
159
+ # subsection is yielded.
151
160
  def each_subsection
152
161
  return to_enum(__method__) unless block_given?
153
162
 
154
163
  temp = []
155
164
  oids.sort.each do |oid|
156
- if !temp.empty? && temp[-1].oid + 1 != oid
157
- yield(temp)
158
- temp = []
165
+ expected_next_oid = !temp.empty? && temp[-1].oid + 1
166
+ if expected_next_oid && expected_next_oid != oid
167
+ if @initial_section
168
+ expected_next_oid.upto(oid - 1) do |free_oid|
169
+ temp << self.class.free_entry(free_oid, 0)
170
+ end
171
+ else
172
+ yield(temp)
173
+ temp = []
174
+ end
159
175
  end
160
176
  temp << self[oid]
161
177
  end
@@ -104,6 +104,11 @@ module CommonTokenizerTests
104
104
  assert_raises(HexaPDF::MalformedPDFError) { @tokenizer.next_token }
105
105
  end
106
106
 
107
+ it "next_token: fails on a closing parenthesis that is not part of a literal string" do
108
+ create_tokenizer(" )")
109
+ assert_raises(HexaPDF::MalformedPDFError) { @tokenizer.next_token }
110
+ end
111
+
107
112
  it "next_token: fails on a missing greater than sign in a hex string" do
108
113
  create_tokenizer("<ABCD")
109
114
  assert_raises(HexaPDF::MalformedPDFError) { @tokenizer.next_token }
@@ -157,6 +157,12 @@ describe HexaPDF::DigitalSignature::Signing::DefaultHandler do
157
157
  assert_same(@obj, @doc.catalog[:Perms][:DocMDP])
158
158
  end
159
159
 
160
+ it "updates the document version if :pades signing is used" do
161
+ @handler.signature_type = :pades
162
+ @handler.finalize_objects(@field, @obj)
163
+ assert_equal('2.0', @doc.version)
164
+ end
165
+
160
166
  it "fails if DocMDP should be set but there is already a signature" do
161
167
  @handler.doc_mdp_permissions = :no_changes
162
168
  2.times do
@@ -62,7 +62,7 @@ describe HexaPDF::DigitalSignature::CMSHandler do
62
62
  @dict.contents = @pkcs7.to_der
63
63
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
64
64
  result = @handler.verify(@store)
65
- assert_equal(2, result.messages.size)
65
+ assert_equal(3, result.messages.size)
66
66
  assert_equal(:error, result.messages.first.type)
67
67
  assert_match(/Exactly one signer needed/, result.messages.first.content)
68
68
  end
@@ -100,13 +100,13 @@ describe HexaPDF::DigitalSignature::CMSHandler do
100
100
 
101
101
  it "verifies the signature itself" do
102
102
  result = @handler.verify(@store)
103
- assert_equal(:info, result.messages.last.type)
104
- assert_match(/Signature valid/, result.messages.last.content)
103
+ assert_equal(:info, result.messages[-2].type)
104
+ assert_match(/Signature valid/, result.messages[-2].content)
105
105
 
106
106
  @dict.signed_data = 'other data'
107
107
  result = @handler.verify(@store)
108
- assert_equal(:error, result.messages.last.type)
109
- assert_match(/Signature verification failed/, result.messages.last.content)
108
+ assert_equal(:error, result.messages[-2].type)
109
+ assert_match(/Signature verification failed/, result.messages[-2].content)
110
110
  end
111
111
 
112
112
  it "verifies a timestamp signature" do
@@ -125,8 +125,13 @@ describe HexaPDF::DigitalSignature::CMSHandler do
125
125
  @handler = HexaPDF::DigitalSignature::CMSHandler.new(@dict)
126
126
 
127
127
  result = @handler.verify(@store)
128
- assert_equal(:info, result.messages.last.type)
129
- assert_match(/Signature valid/, result.messages.last.content)
128
+ assert_equal(:info, result.messages[-2].type)
129
+ assert_match(/Signature valid/, result.messages[-2].content)
130
+ end
131
+
132
+ it "provides information on the certificate chain" do
133
+ result = @handler.verify(@store)
134
+ assert_match(/RSA signer -> HexaPDF Test Root CA/, result.messages.last.content)
130
135
  end
131
136
  end
132
137
 
@@ -111,6 +111,13 @@ describe HexaPDF::DigitalSignature::Signature do
111
111
  assert_equal((MINIMAL_PDF[0, 400] << MINIMAL_PDF[500, 333]).b, @sig.signed_data)
112
112
  end
113
113
 
114
+ it "works for invalid offsets" do
115
+ doc = HexaPDF::Document.new(io: StringIO.new(MINIMAL_PDF))
116
+ @sig.document = doc
117
+ @sig[:ByteRange] = [0, 400, 9000, 333]
118
+ assert_equal(MINIMAL_PDF[0, 400], @sig.signed_data)
119
+ end
120
+
114
121
  it "fails if the document isn't associated with an existing PDF file" do
115
122
  assert_raises(HexaPDF::Error) { @sig.signed_data }
116
123
  end
@@ -70,7 +70,8 @@ describe HexaPDF::DigitalSignature::Signatures do
70
70
  end
71
71
  @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
72
72
  sig = @doc.signatures.first
73
- assert_equal([0, 925, 925 + sig[:Contents].size * 2 + 2, 2501], sig[:ByteRange].value)
73
+ assert_equal([0, 925, 925 + sig[:Contents].size * 2 + 2, 2455 + HexaPDF::VERSION.length],
74
+ sig[:ByteRange].value)
74
75
  assert_equal(:sig, sig[:key])
75
76
  assert_equal(:sig_field, @doc.acro_form.each_field.first[:key])
76
77
  assert(sig.key?(:Contents))
@@ -132,14 +133,18 @@ describe HexaPDF::DigitalSignature::Signatures do
132
133
  it "handles different xref section types correctly when determing the offsets" do
133
134
  @doc.delete(7)
134
135
  sig = @doc.signatures.add(@io, @handler, write_options: {update_fields: false})
135
- assert_equal([0, 1036, 1036 + sig[:Contents].size * 2 + 2, 2483], sig[:ByteRange].value)
136
+ l1 = 1030 + HexaPDF::VERSION.length
137
+ assert_equal([0, l1, l1 + sig[:Contents].size * 2 + 2, 2437 + HexaPDF::VERSION.length],
138
+ sig[:ByteRange].value)
136
139
  end
137
140
 
138
141
  it "works if the signature object is the last object of the xref section" do
139
142
  field = @doc.acro_form(create: true).create_signature_field('Signature2')
140
143
  field.create_widget(@doc.pages[0], Rect: [0, 0, 0, 0])
141
144
  sig = @doc.signatures.add(@io, @handler, signature: field, write_options: {update_fields: false})
142
- assert_equal([0, 3143, 3143 + sig[:Contents].size * 2 + 2, 380], sig[:ByteRange].value)
145
+ l1 = 3097 + HexaPDF::VERSION.length
146
+ assert_equal([0, l1, l1 + sig[:Contents].size * 2 + 2, 374 + HexaPDF::VERSION.length],
147
+ sig[:ByteRange].value)
143
148
  end
144
149
 
145
150
  it "allows writing to a file in addition to writing to an IO" do
@@ -5,7 +5,7 @@ require 'hexapdf/font/cmap/writer'
5
5
 
6
6
  describe HexaPDF::Font::CMap::Writer do
7
7
  before do
8
- @cmap_data = <<~EOF
8
+ @to_unicode_cmap_data = <<~EOF
9
9
  /CIDInit /ProcSet findresource begin
10
10
  12 dict begin
11
11
  begincmap
@@ -32,35 +32,92 @@ describe HexaPDF::Font::CMap::Writer do
32
32
  end
33
33
  end
34
34
  EOF
35
- @mapping = []
36
- 0x00.upto(0x5e) {|i| @mapping << [i, 0x20 + i] }
37
- @mapping << [0x60, 0x90]
38
- 0x1379.upto(0x137B) {|i| @mapping << [i, 0x90FE + i - 0x1379] }
39
- @mapping << [0x3A51, 0x2003E]
35
+ @cid_cmap_data = <<~EOF
36
+ %!PS-Adobe-3.0 Resource-CMap
37
+ %%DocumentNeededResources: ProcSet (CIDInit)
38
+ %%IncludeResource: ProcSet (CIDInit)
39
+ %%BeginResource: CMap (Custom)
40
+ %%Title: (Custom Adobe Identity 0)
41
+ %%Version: 1
42
+ /CIDInit /ProcSet findresource begin
43
+ 12 dict begin
44
+ begincmap
45
+ /CIDSystemInfo 3 dict dup begin
46
+ /Registry (Adobe) def
47
+ /Ordering (Identity) def
48
+ /Supplement 0 def
49
+ end def
50
+ /CMapName /Custom def
51
+ /CMapType 1 def
52
+ /CMapVersion 1 def
53
+ /WMode 0 def
54
+ 1 begincodespacerange
55
+ <0000> <FFFF>
56
+ endcodespacerange
57
+ 1 begincidchar
58
+ <0060> 144
59
+ endcidchar
60
+ 1 begincidrange
61
+ <0000><005E> 32
62
+ endcidrange
63
+ endcmap
64
+ CMapName currentdict /CMap defineresource pop
65
+ end
66
+ end
67
+ %%EndResource
68
+ %%EOF
69
+ EOF
70
+
71
+ @to_unicode_mapping = []
72
+ @cid_mapping = []
73
+ 0x00.upto(0x5e) do |i|
74
+ @to_unicode_mapping << [i, 0x20 + i]
75
+ @cid_mapping << [i, 0x20 + i]
76
+ end
77
+ @to_unicode_mapping << [0x60, 0x90]
78
+ @cid_mapping << [0x60, 0x90]
79
+ 0x1379.upto(0x137B) do |i|
80
+ @to_unicode_mapping << [i, 0x90FE + i - 0x1379]
81
+ end
82
+ @to_unicode_mapping << [0x3A51, 0x2003E]
40
83
  end
41
84
 
42
85
  describe "create_to_unicode_cmap" do
43
86
  it "creates a correct CMap file" do
44
- assert_equal(@cmap_data, HexaPDF::Font::CMap.create_to_unicode_cmap(@mapping))
87
+ assert_equal(@to_unicode_cmap_data,
88
+ HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
45
89
  end
46
90
 
47
91
  it "works if the last item is a range" do
48
- @mapping.pop
49
- @cmap_data.sub!(/2 beginbfchar/, '1 beginbfchar')
50
- @cmap_data.sub!(/<3A51><d840dc3e>\n/, '')
51
- assert_equal(@cmap_data, HexaPDF::Font::CMap.create_to_unicode_cmap(@mapping))
92
+ @to_unicode_mapping.pop
93
+ @to_unicode_cmap_data.sub!(/2 beginbfchar/, '1 beginbfchar')
94
+ @to_unicode_cmap_data.sub!(/<3A51><d840dc3e>\n/, '')
95
+ assert_equal(@to_unicode_cmap_data,
96
+ HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
52
97
  end
53
98
 
54
99
  it "works with only ranges" do
55
- @mapping.delete_at(-1)
56
- @mapping.delete_at(0x5f)
57
- @cmap_data.sub!(/\n2 beginbfchar.*endbfchar/m, '')
58
- assert_equal(@cmap_data, HexaPDF::Font::CMap.create_to_unicode_cmap(@mapping))
100
+ @to_unicode_mapping.delete_at(-1)
101
+ @to_unicode_mapping.delete_at(0x5f)
102
+ @to_unicode_cmap_data.sub!(/\n2 beginbfchar.*endbfchar/m, '')
103
+ assert_equal(@to_unicode_cmap_data,
104
+ HexaPDF::Font::CMap.create_to_unicode_cmap(@to_unicode_mapping))
59
105
  end
60
106
 
61
107
  it "returns an empty CMap if the mapping is empty" do
62
- assert_equal(@cmap_data.sub(/\d+ beginbfchar.*endbfrange/m, ''),
108
+ assert_equal(@to_unicode_cmap_data.sub(/\d+ beginbfchar.*endbfrange/m, ''),
63
109
  HexaPDF::Font::CMap.create_to_unicode_cmap([]))
64
110
  end
65
111
  end
112
+
113
+ describe "create_cid_cmap" do
114
+ it "creates a correct CMap file" do
115
+ assert_equal(@cid_cmap_data, HexaPDF::Font::CMap.create_cid_cmap(@cid_mapping))
116
+ end
117
+
118
+ it "returns an empty CMap if the mapping is empty" do
119
+ assert_equal(@cid_cmap_data.sub(/\d+ begincidchar.*endcidrange/m, ''),
120
+ HexaPDF::Font::CMap.create_cid_cmap([]))
121
+ end
122
+ end
66
123
  end
@@ -71,6 +71,12 @@ describe HexaPDF::Font::TrueTypeWrapper do
71
71
  glyph.inspect)
72
72
  end
73
73
 
74
+ it "caches glyphs based on the id and string" do
75
+ glyph = @font_wrapper.glyph(17)
76
+ assert_same(glyph, @font_wrapper.glyph(17))
77
+ refute_same(glyph, @font_wrapper.glyph(17, "1"))
78
+ end
79
+
74
80
  it "invokes font.on_missing_glyph for missing glyphs" do
75
81
  glyph = @font_wrapper.glyph(9999)
76
82
  assert_kind_of(HexaPDF::Font::InvalidGlyph, glyph)
@@ -99,14 +105,18 @@ describe HexaPDF::Font::TrueTypeWrapper do
99
105
  assert_equal([1].pack('n'), code)
100
106
  code = @font_wrapper.encode(@font_wrapper.glyph(10))
101
107
  assert_equal([2].pack('n'), code)
108
+ code = @font_wrapper.encode(@font_wrapper.glyph(10, "o"))
109
+ assert_equal([3].pack('n'), code)
102
110
  end
103
111
 
104
112
  it "returns the encoded glyph ID for fonts that are not subset" do
105
113
  @font_wrapper = HexaPDF::Font::TrueTypeWrapper.new(@doc, @font, subset: false)
106
114
  code = @font_wrapper.encode(@font_wrapper.glyph(3))
107
- assert_equal([3].pack('n'), code)
115
+ assert_equal([1].pack('n'), code)
108
116
  code = @font_wrapper.encode(@font_wrapper.glyph(10))
109
- assert_equal([10].pack('n'), code)
117
+ assert_equal([2].pack('n'), code)
118
+ code = @font_wrapper.encode(@font_wrapper.glyph(10, "o"))
119
+ assert_equal([3].pack('n'), code)
110
120
  end
111
121
 
112
122
  it "raises an error if an InvalidGlyph is encoded" do
@@ -180,14 +190,18 @@ describe HexaPDF::Font::TrueTypeWrapper do
180
190
  it "with fonts that are not subset (only differences to other case)" do
181
191
  @font_wrapper = HexaPDF::Font::TrueTypeWrapper.new(@doc, @font, subset: false)
182
192
  @font_wrapper.encode(@font_wrapper.glyph(3))
193
+ @font_wrapper.encode(@font_wrapper.glyph(3, "-"))
183
194
  glyph = @font_wrapper.decode_utf8('H').first
184
195
  @font_wrapper.encode(glyph)
185
196
  @doc.dispatch_message(:complete_objects)
186
197
 
187
198
  dict = @font_wrapper.pdf_object
188
199
 
189
- assert_equal(HexaPDF::Font::CMap.create_to_unicode_cmap([[3, ' '.ord], [glyph.id, 'H'.ord]]),
200
+ assert_equal(HexaPDF::Font::CMap.create_to_unicode_cmap([[1, ' '.ord], [2, '-'.ord],
201
+ [3, 'H'.ord]]),
190
202
  dict[:ToUnicode].stream)
203
+ assert_equal(HexaPDF::Font::CMap.create_cid_cmap([[1, 3], [2, 3], [3, glyph.id]]),
204
+ dict[:Encoding].stream)
191
205
  assert_equal([glyph.id, [glyph.width]], dict[:DescendantFonts][0][:W].value)
192
206
  end
193
207
  end
@@ -185,7 +185,7 @@ describe HexaPDF::Layout::ListBox do
185
185
  [:set_font_and_size, [:F1, 11]],
186
186
  [:set_device_gray_non_stroking_color, [0.5]],
187
187
  [:begin_text],
188
- [:set_text_matrix, [1, 0, 0, 1, 1.15, 92.487]],
188
+ [:move_text, [1.15, 92.487]],
189
189
  [:show_text, ["\x95".b]],
190
190
  [:end_text],
191
191
  [:restore_graphics_state],
@@ -197,7 +197,7 @@ describe HexaPDF::Layout::ListBox do
197
197
  [:set_font_and_size, [:F1, 11]],
198
198
  [:set_device_gray_non_stroking_color, [0.5]],
199
199
  [:begin_text],
200
- [:set_text_matrix, [1, 0, 0, 1, 1.15, 82.487]],
200
+ [:move_text, [1.15, 82.487]],
201
201
  [:show_text, ["\x95".b]],
202
202
  [:end_text],
203
203
  [:restore_graphics_state],
@@ -219,7 +219,7 @@ describe HexaPDF::Layout::ListBox do
219
219
  [:set_text_rise, [-6.111111]],
220
220
  [:set_device_gray_non_stroking_color, [0.5]],
221
221
  [:begin_text],
222
- [:set_text_matrix, [1, 0, 0, 1, 0.1985, 100]],
222
+ [:move_text, [0.1985, 100]],
223
223
  [:show_text, ["m".b]],
224
224
  [:end_text],
225
225
  [:restore_graphics_state],
@@ -241,7 +241,7 @@ describe HexaPDF::Layout::ListBox do
241
241
  [:set_text_rise, [-6.111111]],
242
242
  [:set_device_gray_non_stroking_color, [0.5]],
243
243
  [:begin_text],
244
- [:set_text_matrix, [1, 0, 0, 1, 0.8145, 100]],
244
+ [:move_text, [0.8145, 100]],
245
245
  [:show_text, ["n".b]],
246
246
  [:end_text],
247
247
  [:restore_graphics_state],
@@ -263,7 +263,7 @@ describe HexaPDF::Layout::ListBox do
263
263
  [:set_font_and_size, [:F1, 11]],
264
264
  [:set_device_gray_non_stroking_color, [0.5]],
265
265
  [:begin_text],
266
- [:set_text_matrix, [1, 0, 0, 1, 6.75, 92.487]],
266
+ [:move_text, [6.75, 92.487]],
267
267
  [:show_text, ["1.".b]],
268
268
  [:end_text],
269
269
  [:restore_graphics_state],
@@ -275,7 +275,7 @@ describe HexaPDF::Layout::ListBox do
275
275
  [:set_font_and_size, [:F1, 11]],
276
276
  [:set_device_gray_non_stroking_color, [0.5]],
277
277
  [:begin_text],
278
- [:set_text_matrix, [1, 0, 0, 1, 6.75, 82.487]],
278
+ [:move_text, [6.75, 82.487]],
279
279
  [:show_text, ["2.".b]],
280
280
  [:end_text],
281
281
  [:restore_graphics_state],
@@ -314,7 +314,7 @@ describe HexaPDF::Layout::ListBox do
314
314
  [:save_graphics_state],
315
315
  [:set_font_and_size, [:F1, 10]],
316
316
  [:begin_text],
317
- [:set_text_matrix, [1, 0, 0, 1, 1.5, 93.17]],
317
+ [:move_text, [1.5, 93.17]],
318
318
  [:show_text, ["\x95".b]],
319
319
  [:end_text],
320
320
  [:restore_graphics_state],
@@ -139,7 +139,7 @@ describe HexaPDF::Layout::TextFragment do
139
139
  [:set_text_rise, [2]],
140
140
  *middle,
141
141
  [:begin_text],
142
- [:set_text_matrix, [1, 0, 0, 1, 10, 15]],
142
+ [:move_text, [10, 15]],
143
143
  [:show_text, ['!']],
144
144
  *back,
145
145
  ].compact
@@ -156,7 +156,7 @@ describe HexaPDF::Layout::TextFragment do
156
156
  @canvas = @doc.pages.add.canvas
157
157
  @fragment.draw(@canvas, 10, 15, ignore_text_properties: true)
158
158
  assert_operators(@canvas.contents, [[:begin_text],
159
- [:set_text_matrix, [1, 0, 0, 1, 10, 15]]])
159
+ [:move_text, [10, 15]]])
160
160
  end
161
161
 
162
162
  describe "uses an appropriate text position setter" do
@@ -188,7 +188,7 @@ describe HexaPDF::Layout::TextFragment do
188
188
  it "horizontal and vertical movement" do
189
189
  @fragment.draw(@canvas, 10, 10, ignore_text_properties: true)
190
190
  assert_operators(@canvas.contents, [[:begin_text],
191
- [:set_text_matrix, [1, 0, 0, 1, 10, 10]]])
191
+ [:move_text, [10, 10]]])
192
192
  end
193
193
  end
194
194
 
@@ -743,10 +743,12 @@ describe HexaPDF::Layout::TextLayouter do
743
743
  result = processor.recorded_ops
744
744
  leading = (result.select {|name, _| name == :set_leading } || [0]).map(&:last).flatten.first
745
745
  pos = [0, 0]
746
- result.select! {|name, _| name == :set_text_matrix || name == :move_text_next_line }.
747
- map! do |name, ops|
746
+ result.select! do |name, _|
747
+ name == :set_text_matrix || name == :move_text || name == :move_text_next_line
748
+ end.map! do |name, ops|
748
749
  case name
749
750
  when :set_text_matrix then pos = ops[-2, 2]
751
+ when :move_text then pos = ops
750
752
  when :move_text_next_line then pos[1] -= leading
751
753
  end
752
754
  pos.dup