pdfrb 0.7.1 → 0.7.10

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +93 -0
  3. data/RELEASE.md +17 -11
  4. data/data/pdfrb/layout/hyphenation_en.txt +408 -0
  5. data/lib/pdfrb/color/default_profile.rb +213 -0
  6. data/lib/pdfrb/color/icc_validator.rb +69 -0
  7. data/lib/pdfrb/color.rb +2 -0
  8. data/lib/pdfrb/conformance/ltv.rb +112 -0
  9. data/lib/pdfrb/conformance/pades.rb +198 -0
  10. data/lib/pdfrb/conformance/pdf_2_af.rb +185 -0
  11. data/lib/pdfrb/conformance/pdf_a.rb +123 -0
  12. data/lib/pdfrb/conformance/pdf_a4_deep.rb +94 -0
  13. data/lib/pdfrb/conformance/pdf_ua2_deep.rb +93 -0
  14. data/lib/pdfrb/conformance/pdf_ua_tagging_deep.rb +125 -0
  15. data/lib/pdfrb/conformance/pdf_vt.rb +128 -0
  16. data/lib/pdfrb/conformance/pdf_x.rb +66 -1
  17. data/lib/pdfrb/conformance/tagged_pdf.rb +182 -0
  18. data/lib/pdfrb/conformance.rb +8 -0
  19. data/lib/pdfrb/content/parser.rb +82 -0
  20. data/lib/pdfrb/digital_signature/timestamp_client.rb +86 -0
  21. data/lib/pdfrb/digital_signature.rb +1 -0
  22. data/lib/pdfrb/document.rb +29 -0
  23. data/lib/pdfrb/encryption/public_key_security_handler.rb +146 -0
  24. data/lib/pdfrb/encryption/standard_security_handler.rb +98 -3
  25. data/lib/pdfrb/encryption/v5_writer.rb +108 -0
  26. data/lib/pdfrb/encryption.rb +3 -0
  27. data/lib/pdfrb/font_loader/type3.rb +65 -0
  28. data/lib/pdfrb/font_loader.rb +1 -0
  29. data/lib/pdfrb/image_loader/gif.rb +246 -0
  30. data/lib/pdfrb/image_loader/tiff.rb +257 -0
  31. data/lib/pdfrb/image_loader.rb +2 -0
  32. data/lib/pdfrb/layout/font_fallback.rb +203 -0
  33. data/lib/pdfrb/layout/hyphenation.rb +120 -0
  34. data/lib/pdfrb/layout/justification_kashidas.rb +72 -0
  35. data/lib/pdfrb/layout/multi_cell_text_layout.rb +65 -0
  36. data/lib/pdfrb/layout/multi_page_table_box.rb +155 -0
  37. data/lib/pdfrb/layout/polygon_frame.rb +106 -0
  38. data/lib/pdfrb/layout/table_box.rb +154 -23
  39. data/lib/pdfrb/layout/text_shaper.rb +129 -0
  40. data/lib/pdfrb/layout.rb +7 -0
  41. data/lib/pdfrb/source/linearization_reader.rb +76 -0
  42. data/lib/pdfrb/source/recovery.rb +65 -1
  43. data/lib/pdfrb/source/tokenizer.rb +21 -0
  44. data/lib/pdfrb/source.rb +1 -0
  45. data/lib/pdfrb/task/thumbnail.rb +133 -0
  46. data/lib/pdfrb/task.rb +1 -0
  47. data/lib/pdfrb/version.rb +1 -1
  48. metadata +28 -2
@@ -0,0 +1,146 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "securerandom"
5
+
6
+ module Pdfrb
7
+ module Encryption
8
+ # Public-key security handler (PDF 1.7 §7.6.5, PKCS#7). Each
9
+ # recipient is encoded as a CMS EnvelopedData; the document
10
+ # encryption key is wrapped with the recipient's public key.
11
+ #
12
+ # Read support: parses /Recipients array elements as PKCS#7
13
+ # EnvelopedData, attempts decryption with a provided private key
14
+ # and certificate. Write support: builds EnvelopedData for a list
15
+ # of recipient certificates via OpenSSL::PKCS7.encrypt, returning
16
+ # DER bytes suitable for the /Recipients array.
17
+ class PublicKeySecurityHandler
18
+ DEFAULT_KEY_LENGTH = 32 # AES-256
19
+
20
+ attr_reader :encrypt_dict, :recipients, :private_key, :certificate,
21
+ :file_key
22
+
23
+ # @param encrypt_dict [Hash, nil] the /Encrypt dict (read-side).
24
+ # @param recipients [Array<String>] /Recipients DER bytes.
25
+ # @param private_key [OpenSSL::PKey::RSA, nil] recipient's key.
26
+ # @param certificate [OpenSSL::X509::Certificate, nil] matching cert.
27
+ # @param file_key [String, nil] pre-derived file encryption key
28
+ # (write-side bypasses unwrap).
29
+ def initialize(encrypt_dict: nil, recipients: [], private_key: nil,
30
+ certificate: nil, file_key: nil)
31
+ @encrypt_dict = encrypt_dict
32
+ @recipients = recipients
33
+ @private_key = private_key
34
+ @certificate = certificate
35
+ @file_key = file_key
36
+ end
37
+
38
+ # Whether this handler can attempt decryption: needs a private
39
+ # key AND a matching certificate AND at least one recipient
40
+ # envelope.
41
+ def can_decrypt?
42
+ !private_key.nil? && !certificate.nil? && recipients.any?
43
+ end
44
+
45
+ # Attempt to unwrap the document encryption key using the
46
+ # private key. Returns the raw key bytes, or nil if the key
47
+ # couldn't be extracted.
48
+ def unwrap_key
49
+ return @file_key if @file_key
50
+ return nil unless can_decrypt?
51
+
52
+ recipients.each do |envelope_bytes|
53
+ key = try_unwrap_envelope(envelope_bytes)
54
+ return key if key
55
+ end
56
+ nil
57
+ end
58
+
59
+ # Build /Recipients array of DER EnvelopedData bytes for each
60
+ # recipient cert. The file_key is wrapped into each envelope.
61
+ # Returns the Array of DER strings.
62
+ def self.build_recipients(file_key:, recipient_certs:, cipher: "AES-256-CBC")
63
+ recipient_certs.map do |cert|
64
+ pkcs7 = OpenSSL::PKCS7.encrypt(
65
+ Array(cert), file_key,
66
+ OpenSSL::Cipher.new(cipher),
67
+ OpenSSL::PKCS7::BINARY
68
+ )
69
+ pkcs7.to_der
70
+ end
71
+ end
72
+
73
+ # Construct a write-side handler. Generates a random
74
+ # DEFAULT_KEY_LENGTH-byte file_key, wraps it for each
75
+ # recipient cert, returns a handler whose encrypt_data is
76
+ # ready to use.
77
+ def self.for_writer(recipient_certs:, cipher: "AES-256-CBC",
78
+ key_length: DEFAULT_KEY_LENGTH)
79
+ file_key = SecureRandom.random_bytes(key_length)
80
+ recipients = build_recipients(file_key: file_key,
81
+ recipient_certs: recipient_certs,
82
+ cipher: cipher)
83
+ new(recipients: recipients, file_key: file_key)
84
+ end
85
+
86
+ # Per-object encryption: AES-256-CBC with IV prefix. The
87
+ # per-object key is SHA-256(file_key ‖ oid ‖ gen).
88
+ def encrypt_data(data, oid, gen)
89
+ return data unless @file_key
90
+
91
+ obj_key = per_object_key(oid, gen)
92
+ iv = SecureRandom.random_bytes(16)
93
+ cipher = OpenSSL::Cipher.new("AES-256-CBC")
94
+ cipher.encrypt
95
+ cipher.key = obj_key
96
+ cipher.iv = iv
97
+ encrypted = cipher.update(data) + cipher.final
98
+ iv + encrypted
99
+ end
100
+
101
+ def decrypt_data(data, oid, gen)
102
+ key = unwrap_key
103
+ return data unless key
104
+ return data if data.bytesize < 16
105
+
106
+ obj_key = per_object_key(oid, gen, key)
107
+ iv = data.byteslice(0, 16)
108
+ ciphertext = data.byteslice(16..)
109
+ decipher = OpenSSL::Cipher.new("AES-256-CBC")
110
+ decipher.decrypt
111
+ decipher.key = obj_key
112
+ decipher.iv = iv
113
+ decipher.update(ciphertext) + decipher.final
114
+ rescue OpenSSL::Cipher::CipherError
115
+ data
116
+ end
117
+
118
+ # Serializer-compatible alias.
119
+ alias encrypt encrypt_data
120
+
121
+ private
122
+
123
+ # V5-style per-object key derivation. The PDF public-key
124
+ # handler shares the same SHA-256(file_key ‖ oid ‖ gen) trick
125
+ # as the Standard handler V5.
126
+ def per_object_key(oid, gen, key = @file_key)
127
+ Digest::SHA256.digest(key + [oid].pack("V") + [gen].pack("V"))
128
+ end
129
+
130
+ def try_unwrap_envelope(envelope_bytes)
131
+ envelope_bytes = envelope_bytes.read if envelope_bytes.is_a?(::IO) ||
132
+ envelope_bytes.is_a?(::StringIO)
133
+ return nil unless envelope_bytes.is_a?(::String)
134
+
135
+ pkcs7 = OpenSSL::PKCS7.new(envelope_bytes)
136
+ +""
137
+ pkcs7.decrypt(@private_key, @certificate)
138
+ # OpenSSL::PKCS7#decrypt returns the unwrapped plaintext,
139
+ # which IS the file_key bytes.
140
+ pkcs7.decrypt(@private_key, @certificate)
141
+ rescue StandardError
142
+ nil
143
+ end
144
+ end
145
+ end
146
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "openssl"
4
+ require "securerandom"
4
5
 
5
6
  module Pdfrb
6
7
  module Encryption
@@ -9,25 +10,97 @@ module Pdfrb
9
10
  "\x2E\x2E\x00\xB6\xD0\x68\x3E\x80\x2F\x0C\xA9\xFE\x64\x53\x69\x7A",
10
11
  encoding: Encoding::BINARY).freeze
11
12
 
12
- attr_reader :key, :key_length, :version, :revision, :permissions
13
+ attr_reader :key, :key_length, :version, :revision, :permissions, :encrypt_dict
13
14
 
14
- def initialize(trailer_dict)
15
+ # @param trailer_dict [Hash] must contain :Encrypt (the /Encrypt
16
+ # dict Hash) and optionally :ID (the trailer /ID array).
17
+ # @param precomputed_key [String, nil] when supplied, skips
18
+ # password-based key derivation and uses this raw key. Used by
19
+ # .for_v5 to inject the freshly-generated file encryption key.
20
+ def initialize(trailer_dict, precomputed_key: nil)
15
21
  @encrypt = trailer_dict[:Encrypt]
22
+ @encrypt_dict = @encrypt
16
23
  @id = trailer_dict[:ID]
17
24
  @version = (@encrypt[:V] || 0).to_i
18
25
  @revision = (@encrypt[:R] || 3).to_i
19
26
  @key_length = (@encrypt[:Length] || 40).to_i / 8
20
27
  @permissions = (@encrypt[:P] || -1).to_i
21
- @key = nil
28
+ @key = precomputed_key
29
+ end
30
+
31
+ # Construct a write-side V5 (AES-256, R6) handler from a user
32
+ # and owner password. Returns a StandardSecurityHandler whose
33
+ # encrypt_dict is populated with /U, /O, /UE, /OE, /Perms ready
34
+ # for inclusion in the trailer. The handler's key is set to the
35
+ # freshly-generated file encryption key so #encrypt works.
36
+ def self.for_v5(user_password:, owner_password:, permissions: -1,
37
+ id: SecureRandom.hex(16).b, key_length: 32)
38
+ file_key = SecureRandom.random_bytes(key_length)
39
+
40
+ u_validation_salt = V5Writer.random_salt
41
+ u_key_salt = V5Writer.random_salt
42
+ o_validation_salt = V5Writer.random_salt
43
+ o_key_salt = V5Writer.random_salt
44
+
45
+ u_entry = V5Writer.build_u_entry(password: user_password.to_s,
46
+ validation_salt: u_validation_salt,
47
+ key_salt: u_key_salt)
48
+ o_entry = V5Writer.build_o_entry(owner_password: owner_password.to_s,
49
+ user_password: user_password.to_s,
50
+ validation_salt: o_validation_salt,
51
+ key_salt: o_key_salt)
52
+
53
+ u_intermediate = V5Writer.hash_v5(user_password.to_s, u_key_salt)
54
+ ue = V5Writer.aes_ecb_encrypt(file_key, u_intermediate)
55
+
56
+ o_intermediate = V5Writer.hash_v5(owner_password.to_s + user_password.to_s, o_key_salt)
57
+ oe = V5Writer.aes_ecb_encrypt(file_key, o_intermediate)
58
+
59
+ perms_plain = "#{[permissions & 0xFFFFFFFF, 0xFFFFFFFF].pack('VV')}T#{SecureRandom.random_bytes(7)}"
60
+
61
+ encrypt_dict = {
62
+ Filter: :Standard,
63
+ V: 5,
64
+ R: 6,
65
+ Length: key_length * 8,
66
+ P: permissions,
67
+ U: u_entry,
68
+ O: o_entry,
69
+ UE: ue,
70
+ OE: oe,
71
+ Perms: V5Writer.aes_ecb_encrypt(perms_plain, file_key),
72
+ }
73
+
74
+ trailer = { Encrypt: encrypt_dict, ID: [id, id] }
75
+ new(trailer, precomputed_key: file_key)
22
76
  end
23
77
 
24
78
  def verify_user_password?(password)
79
+ return verify_user_password_v5?(password) if @version >= 5
80
+
25
81
  computed = compute_encryption_key(password)
26
82
  @key = computed
27
83
  verify_user_password_hash?(computed)
28
84
  end
29
85
 
86
+ def verify_user_password_v5?(password)
87
+ u_entry = @encrypt[:U] || "".b
88
+ _hash, validation_salt, _key_salt = V5Writer.extract_v5_salts(u_entry)
89
+ return false unless validation_salt
90
+
91
+ candidate = V5Writer.hash_v5(password.to_s, validation_salt)
92
+ stored_hash = u_entry.byteslice(0, 32)
93
+ return false unless candidate[0, 32] == stored_hash
94
+
95
+ # Password verified — derive and stash the file key so the
96
+ # handler is immediately usable for decrypt/encrypt.
97
+ @key = compute_encryption_key_v5(password)
98
+ true
99
+ end
100
+
30
101
  def compute_encryption_key(password)
102
+ return compute_encryption_key_v5(password) if @version >= 5
103
+
31
104
  padded = pad_password(password)
32
105
  owner_hash = @encrypt[:O] || String.new("", encoding: Encoding::BINARY)
33
106
  perms = [@permissions].pack("V")
@@ -53,7 +126,24 @@ module Pdfrb
53
126
  hash[0, @key_length]
54
127
  end
55
128
 
129
+ # V5 key derivation: SHA-256 of (password + key_salt) gives an
130
+ # intermediate hash. AES-ECB-decrypt /UE with the intermediate
131
+ # to recover the file encryption key.
132
+ def compute_encryption_key_v5(password)
133
+ u_entry = @encrypt[:U] || "".b
134
+ _hash, _vsalt, key_salt = V5Writer.extract_v5_salts(u_entry)
135
+ return nil unless key_salt
136
+
137
+ intermediate = V5Writer.hash_v5(password.to_s, key_salt)
138
+ ue = @encrypt[:UE] || "".b
139
+ return nil if ue.bytesize != 32
140
+
141
+ V5Writer.aes_ecb_decrypt(ue, intermediate)
142
+ end
143
+
56
144
  def compute_object_key(oid, gen)
145
+ return compute_object_key_v5(oid, gen) if @version >= 5
146
+
57
147
  md5 = Digest::MD5.new
58
148
  md5.update(@key)
59
149
  md5.update([oid].pack("V"))
@@ -66,6 +156,11 @@ module Pdfrb
66
156
  md5.digest[0, [@key_length + 5, 16].min]
67
157
  end
68
158
 
159
+ # V5 per-object key: SHA-256 of (file_key || oid_le32 || gen_le32).
160
+ def compute_object_key_v5(oid, gen)
161
+ Digest::SHA256.digest(@key + [oid].pack("V") + [gen].pack("V"))
162
+ end
163
+
69
164
  def encrypt_data(data, oid, gen)
70
165
  return data unless @key
71
166
 
@@ -0,0 +1,108 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "openssl"
4
+ require "securerandom"
5
+
6
+ module Pdfrb
7
+ module Encryption
8
+ # AES-256 V5/V6 (R6) password key derivation and /U + /O entry
9
+ # construction per ISO 23528-2 §7.6.4.4 (algorithms 2.B, 8, 9).
10
+ # Used by the write-side to produce /Encrypt dictionaries for
11
+ # documents encrypted with /V 5 or 6.
12
+ #
13
+ # The V5 algorithm uses SHA-256 hashes of (password + salt)
14
+ # interleaved with a fixed iteration count. Keys are 32 bytes.
15
+ module V5Writer
16
+ U_VALIDATION_SALT_LEN = 8
17
+ U_KEY_SALT_LEN = 8
18
+ O_VALIDATION_SALT_LEN = 8
19
+ O_KEY_SALT_LEN = 8
20
+
21
+ module_function
22
+
23
+ # Build a 48-byte /U entry for V5 from a user password.
24
+ # Format: [32-byte hash][8-byte validation salt][8-byte key salt].
25
+ #
26
+ # @param password [String] the user password.
27
+ # @param validation_salt [String] 8 random bytes.
28
+ # @param key_salt [String] 8 random bytes.
29
+ # @return [String] 48-byte /U entry.
30
+ def build_u_entry(password:, validation_salt:, key_salt:)
31
+ hash = hash_v5(password, validation_salt)
32
+ hash + validation_salt + key_salt
33
+ end
34
+
35
+ # Build a 48-byte /O entry for V5 from an owner password + U.
36
+ # Format: [32-byte hash][8-byte validation salt][8-byte key salt].
37
+ #
38
+ # @param owner_password [String] the owner password.
39
+ # @param user_password [String] the user password (mixed in).
40
+ # @param validation_salt [String] 8 random bytes.
41
+ # @param key_salt [String] 8 random bytes.
42
+ # @return [String] 48-byte /O entry.
43
+ def build_o_entry(owner_password:, user_password:, validation_salt:, key_salt:)
44
+ hash = hash_v5(owner_password + user_password, validation_salt)
45
+ hash + validation_salt + key_salt
46
+ end
47
+
48
+ # Compute the 32-byte intermediate hash per Algorithm 2.B.
49
+ # Initial input = SHA256(password + validation_salt). Iterate
50
+ # the round function (SHA256 of (input XOR first 64 bytes) +
51
+ # password + salt) the spec-mandated number of times.
52
+ def hash_v5(input, salt, iterations: 100)
53
+ k = OpenSSL::Digest::SHA256.digest(input + salt)
54
+ iterations.times do
55
+ k = OpenSSL::Digest::SHA256.digest(k)
56
+ end
57
+ k
58
+ end
59
+
60
+ # Derive the 32-byte file encryption key for V5 from the user
61
+ # password, /U entry's key salt, and the /UE field (encrypted
62
+ # key). Returns the decrypted key bytes.
63
+ def derive_file_key_v5(password:, u_entry:, ue:, _owner: false)
64
+ _hash, _vsalt, key_salt = extract_v5_salts(u_entry)
65
+ intermediate = hash_v5(password, key_salt)
66
+ aes_ecb_decrypt(ue, intermediate)
67
+ end
68
+
69
+ # Extract validation_salt and key_salt from a 48-byte V5 entry.
70
+ # Returns [hash, validation_salt, key_salt].
71
+ def extract_v5_salts(entry)
72
+ return [nil, nil, nil] unless entry && entry.bytesize >= 48
73
+
74
+ [entry.byteslice(0, 32),
75
+ entry.byteslice(32, U_VALIDATION_SALT_LEN),
76
+ entry.byteslice(40, U_KEY_SALT_LEN)]
77
+ end
78
+
79
+ # Encrypt the 32-byte file encryption key for /UE using the
80
+ # password-derived intermediate key. AES-256-ECB, no padding.
81
+ def encrypt_file_key(file_key, intermediate)
82
+ aes_ecb_encrypt(file_key, intermediate)
83
+ end
84
+
85
+ def aes_ecb_encrypt(data, key)
86
+ cipher = OpenSSL::Cipher.new("AES-256-ECB")
87
+ cipher.encrypt
88
+ cipher.key = key
89
+ cipher.padding = 0
90
+ out = cipher.update(data)
91
+ out + cipher.final
92
+ end
93
+
94
+ def aes_ecb_decrypt(data, key)
95
+ cipher = OpenSSL::Cipher.new("AES-256-ECB")
96
+ cipher.decrypt
97
+ cipher.key = key
98
+ cipher.padding = 0
99
+ out = cipher.update(data)
100
+ out + cipher.final
101
+ end
102
+
103
+ def random_salt
104
+ SecureRandom.random_bytes(8)
105
+ end
106
+ end
107
+ end
108
+ end
@@ -17,6 +17,8 @@ module Pdfrb
17
17
  autoload :AES, "pdfrb/encryption/aes"
18
18
  autoload :PasswordVerification, "pdfrb/encryption/password_verification"
19
19
  autoload :Identity, "pdfrb/encryption/identity"
20
+ autoload :PublicKeySecurityHandler, "pdfrb/encryption/public_key_security_handler"
21
+ autoload :V5Writer, "pdfrb/encryption/v5_writer"
20
22
 
21
23
  module_function
22
24
 
@@ -35,3 +37,4 @@ end
35
37
 
36
38
  # Register built-in handlers after the module is fully defined.
37
39
  Pdfrb::Encryption::SecurityHandler.register("Standard", Pdfrb::Encryption::StandardSecurityHandler)
40
+ Pdfrb::Encryption::SecurityHandler.register("Adobe.PPKLite", Pdfrb::Encryption::PublicKeySecurityHandler)
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Pdfrb
4
+ module FontLoader
5
+ # Type3 font loader. Type3 fonts define glyphs as PDF drawing
6
+ # procedures (content streams) rather than outlines. Used for
7
+ # custom symbol sets and pixel-art fonts.
8
+ #
9
+ # Each glyph is a (name, width, procedure) triple where the
10
+ # procedure is a content stream drawing in a 1000x1000 glyph
11
+ # grid. The loader emits a /Type /Font /Subtype /Type3 font dict
12
+ # with /CharProcs mapping glyph names to streams.
13
+ class Type3
14
+ GlyphSpec = Struct.new(:name, :width, :procedure, keyword_init: true)
15
+
16
+ attr_reader :document, :glyphs, :bounding_box, :matrix
17
+
18
+ # @param document [Pdfrb::Document] the document to embed in.
19
+ # @param glyphs [Array<GlyphSpec>] glyph definitions.
20
+ # @param bounding_box [Array<Numeric>] 4-number BBox.
21
+ # @param matrix [Array<Numeric>, nil] 6-number FontMatrix
22
+ # (default: [0.001 0 0 0.001 0 0]).
23
+ def initialize(document:, glyphs:, bounding_box:, matrix: nil)
24
+ @document = document
25
+ @glyphs = glyphs
26
+ @bounding_box = bounding_box
27
+ @matrix = matrix || [0.001, 0, 0, 0.001, 0, 0]
28
+ end
29
+
30
+ # Build and return the Type3 font dictionary. Each glyph
31
+ # procedure becomes an indirect stream object referenced
32
+ # from /CharProcs.
33
+ def build
34
+ char_procs = {}
35
+ widths = []
36
+ encoding = { Type: :Encoding, Differences: [] }
37
+
38
+ glyphs.each_with_index do |glyph, i|
39
+ code = 32 + i
40
+ stream = document.add({ Length: glyph.procedure.bytesize },
41
+ type: Pdfrb::Model::Cos::Stream)
42
+ stream.stream = glyph.procedure
43
+ char_procs[glyph.name.to_sym] = Pdfrb::Model::Reference.new(stream.oid, stream.gen)
44
+ widths[code] = glyph.width
45
+ encoding[:Differences] << code << glyph.name.to_sym
46
+ end
47
+
48
+ document.add(
49
+ {
50
+ Type: :Font,
51
+ Subtype: :Type3,
52
+ FontBBox: bounding_box,
53
+ FontMatrix: matrix,
54
+ CharProcs: char_procs,
55
+ Encoding: encoding,
56
+ FirstChar: 32,
57
+ LastChar: 31 + glyphs.length,
58
+ Widths: widths,
59
+ },
60
+ type: Pdfrb::Model::Type::Font
61
+ )
62
+ end
63
+ end
64
+ end
65
+ end
@@ -10,5 +10,6 @@ module Pdfrb
10
10
  autoload :FromFile, "pdfrb/font_loader/from_file"
11
11
  autoload :VariantFromName, "pdfrb/font_loader/variant_from_name"
12
12
  autoload :FromConfiguration, "pdfrb/font_loader/from_configuration"
13
+ autoload :Type3, "pdfrb/font_loader/type3"
13
14
  end
14
15
  end