pdfrb 0.7.1 → 0.7.11
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 +4 -4
- data/CHANGELOG.md +93 -0
- data/RELEASE.md +17 -11
- data/data/pdfrb/layout/hyphenation_en.txt +408 -0
- data/lib/pdfrb/color/default_profile.rb +213 -0
- data/lib/pdfrb/color/icc_validator.rb +69 -0
- data/lib/pdfrb/color.rb +2 -0
- data/lib/pdfrb/conformance/ltv.rb +112 -0
- data/lib/pdfrb/conformance/pades.rb +198 -0
- data/lib/pdfrb/conformance/pdf_2_af.rb +185 -0
- data/lib/pdfrb/conformance/pdf_a.rb +123 -0
- data/lib/pdfrb/conformance/pdf_a4_deep.rb +94 -0
- data/lib/pdfrb/conformance/pdf_ua2_deep.rb +93 -0
- data/lib/pdfrb/conformance/pdf_ua_tagging_deep.rb +125 -0
- data/lib/pdfrb/conformance/pdf_vt.rb +128 -0
- data/lib/pdfrb/conformance/pdf_x.rb +66 -1
- data/lib/pdfrb/conformance/tagged_pdf.rb +182 -0
- data/lib/pdfrb/conformance.rb +8 -0
- data/lib/pdfrb/content/parser.rb +82 -0
- data/lib/pdfrb/digital_signature/timestamp_client.rb +86 -0
- data/lib/pdfrb/digital_signature.rb +1 -0
- data/lib/pdfrb/document.rb +29 -0
- data/lib/pdfrb/encryption/public_key_security_handler.rb +146 -0
- data/lib/pdfrb/encryption/standard_security_handler.rb +98 -3
- data/lib/pdfrb/encryption/v5_writer.rb +108 -0
- data/lib/pdfrb/encryption.rb +3 -0
- data/lib/pdfrb/font_loader/type3.rb +65 -0
- data/lib/pdfrb/font_loader.rb +1 -0
- data/lib/pdfrb/image_loader/gif.rb +246 -0
- data/lib/pdfrb/image_loader/tiff.rb +257 -0
- data/lib/pdfrb/image_loader.rb +2 -0
- data/lib/pdfrb/layout/font_fallback.rb +203 -0
- data/lib/pdfrb/layout/hyphenation.rb +120 -0
- data/lib/pdfrb/layout/justification_kashidas.rb +72 -0
- data/lib/pdfrb/layout/multi_cell_text_layout.rb +65 -0
- data/lib/pdfrb/layout/multi_page_table_box.rb +155 -0
- data/lib/pdfrb/layout/polygon_frame.rb +106 -0
- data/lib/pdfrb/layout/table_box.rb +154 -23
- data/lib/pdfrb/layout/text_shaper.rb +129 -0
- data/lib/pdfrb/layout.rb +7 -0
- data/lib/pdfrb/source/linearization_reader.rb +76 -0
- data/lib/pdfrb/source/recovery.rb +65 -1
- data/lib/pdfrb/source/tokenizer.rb +21 -0
- data/lib/pdfrb/source.rb +1 -0
- data/lib/pdfrb/task/thumbnail.rb +133 -0
- data/lib/pdfrb/task.rb +1 -0
- data/lib/pdfrb/version.rb +1 -1
- metadata +28 -2
|
@@ -0,0 +1,246 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module Pdfrb
|
|
6
|
+
module ImageLoader
|
|
7
|
+
# GIF image loader with full GIF-spec LZW decode. Supports:
|
|
8
|
+
#
|
|
9
|
+
# * Format detection via the "GIF8" magic.
|
|
10
|
+
# * Logical screen descriptor + global color table parse.
|
|
11
|
+
# * Image descriptor (per-frame) for the first image frame.
|
|
12
|
+
# * GIF-LZW decompression (variable-width codes, dictionary
|
|
13
|
+
# rebuild on clear code, dict-reset semantics).
|
|
14
|
+
#
|
|
15
|
+
# Output: an Indexed PDF image XObject whose /ColorSpace is
|
|
16
|
+
# [/Indexed /DeviceRGB <palette bytes>] and whose stream is the
|
|
17
|
+
# decoded palette-index bytes (one byte per pixel), suitable for
|
|
18
|
+
# /FlateDecode re-compression.
|
|
19
|
+
module GIF
|
|
20
|
+
module_function
|
|
21
|
+
|
|
22
|
+
def call(document, data, **_opts)
|
|
23
|
+
data = data.read if data.is_a?(IO) || data.is_a?(StringIO)
|
|
24
|
+
info = parse_header(data)
|
|
25
|
+
return nil if info.empty?
|
|
26
|
+
|
|
27
|
+
palette = read_global_color_table(data, info)
|
|
28
|
+
return nil unless palette
|
|
29
|
+
|
|
30
|
+
frame = parse_first_image_descriptor(data, info)
|
|
31
|
+
return nil unless frame
|
|
32
|
+
|
|
33
|
+
lzw_min = data.getbyte(frame[:lzw_min_code_offset])
|
|
34
|
+
compressed_start = frame[:lzw_min_code_offset] + 1
|
|
35
|
+
compressed = extract_subblocks(data, compressed_start)
|
|
36
|
+
return nil unless compressed
|
|
37
|
+
|
|
38
|
+
indices = lzw_decode(compressed, lzw_min)
|
|
39
|
+
return nil unless indices
|
|
40
|
+
|
|
41
|
+
rgb_palette = palette.pack("C*")
|
|
42
|
+
cs = [:Indexed, :DeviceRGB, (palette.length / 3) - 1, rgb_palette]
|
|
43
|
+
compressed_indices = Zlib.deflate(indices)
|
|
44
|
+
|
|
45
|
+
image = document.add(
|
|
46
|
+
{
|
|
47
|
+
Type: :XObject, Subtype: :Image,
|
|
48
|
+
Width: frame[:width], Height: frame[:height],
|
|
49
|
+
BitsPerComponent: 8,
|
|
50
|
+
ColorSpace: cs,
|
|
51
|
+
Filter: :FlateDecode,
|
|
52
|
+
Length: compressed_indices.bytesize
|
|
53
|
+
},
|
|
54
|
+
type: Pdfrb::Model::Type::XObjectImage
|
|
55
|
+
)
|
|
56
|
+
image.stream = compressed_indices
|
|
57
|
+
image
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def parse_header(data)
|
|
61
|
+
return {} unless data.is_a?(::String) && data.bytesize >= 13
|
|
62
|
+
return {} unless data.start_with?("GIF87a", "GIF89a")
|
|
63
|
+
|
|
64
|
+
width = (data.getbyte(7) << 8) | data.getbyte(6)
|
|
65
|
+
height = (data.getbyte(9) << 8) | data.getbyte(8)
|
|
66
|
+
packed = data.getbyte(10)
|
|
67
|
+
global_ct_flag = packed.anybits?(0x80)
|
|
68
|
+
global_ct_size = global_ct_flag ? (2**((packed & 0x07) + 1)) : 0
|
|
69
|
+
bg_color = data.getbyte(11)
|
|
70
|
+
aspect_ratio = data.getbyte(12)
|
|
71
|
+
{
|
|
72
|
+
width: width,
|
|
73
|
+
height: height,
|
|
74
|
+
global_ct_flag: global_ct_flag,
|
|
75
|
+
global_ct_size: global_ct_size,
|
|
76
|
+
bg_color: bg_color,
|
|
77
|
+
aspect_ratio: aspect_ratio,
|
|
78
|
+
global_ct_offset: 13,
|
|
79
|
+
}
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def read_global_color_table(data, info)
|
|
83
|
+
return nil unless info[:global_ct_flag]
|
|
84
|
+
|
|
85
|
+
n = info[:global_ct_size]
|
|
86
|
+
offset = info[:global_ct_offset]
|
|
87
|
+
return nil unless offset + (n * 3) <= data.bytesize
|
|
88
|
+
|
|
89
|
+
(0...(n * 3)).map { |i| data.getbyte(offset + i) }
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Walk the GIF looking for the first image descriptor (0x2C).
|
|
93
|
+
# Returns a Hash with image descriptor fields + the LZW min
|
|
94
|
+
# code size offset within the data.
|
|
95
|
+
def parse_first_image_descriptor(data, info)
|
|
96
|
+
offset = info[:global_ct_offset] + (info[:global_ct_size] * 3)
|
|
97
|
+
while offset < data.bytesize
|
|
98
|
+
marker = data.getbyte(offset)
|
|
99
|
+
case marker
|
|
100
|
+
when 0x2C
|
|
101
|
+
return parse_image_descriptor(data, offset + 1)
|
|
102
|
+
when 0x21
|
|
103
|
+
offset += 2
|
|
104
|
+
while offset < data.bytesize
|
|
105
|
+
block_size = data.getbyte(offset)
|
|
106
|
+
offset += 1
|
|
107
|
+
break if block_size.zero?
|
|
108
|
+
|
|
109
|
+
offset += block_size
|
|
110
|
+
end
|
|
111
|
+
when 0x3B
|
|
112
|
+
return nil
|
|
113
|
+
else
|
|
114
|
+
offset += 1
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
nil
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def parse_image_descriptor(data, offset)
|
|
121
|
+
return nil if offset + 9 > data.bytesize
|
|
122
|
+
|
|
123
|
+
left = (data.getbyte(offset + 1) << 8) | data.getbyte(offset)
|
|
124
|
+
top = (data.getbyte(offset + 3) << 8) | data.getbyte(offset + 2)
|
|
125
|
+
width = (data.getbyte(offset + 5) << 8) | data.getbyte(offset + 4)
|
|
126
|
+
height = (data.getbyte(offset + 7) << 8) | data.getbyte(offset + 6)
|
|
127
|
+
packed = data.getbyte(offset + 8)
|
|
128
|
+
local_ct_flag = packed.anybits?(0x80)
|
|
129
|
+
local_ct_size = local_ct_flag ? (2**((packed & 0x07) + 1)) : 0
|
|
130
|
+
body_offset = offset + 9 + (local_ct_size * 3)
|
|
131
|
+
{
|
|
132
|
+
left: left, top: top, width: width, height: height,
|
|
133
|
+
local_ct_flag: local_ct_flag, local_ct_size: local_ct_size,
|
|
134
|
+
lzw_min_code_offset: body_offset
|
|
135
|
+
}
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Collect sub-blocks (length-prefixed) starting at +offset+
|
|
139
|
+
# until a zero-length terminator. Returns concatenated bytes.
|
|
140
|
+
def extract_subblocks(data, offset)
|
|
141
|
+
bytes = +"".b
|
|
142
|
+
while offset < data.bytesize
|
|
143
|
+
block_size = data.getbyte(offset)
|
|
144
|
+
offset += 1
|
|
145
|
+
break if block_size.zero?
|
|
146
|
+
|
|
147
|
+
bytes << data.byteslice(offset, block_size)
|
|
148
|
+
offset += block_size
|
|
149
|
+
end
|
|
150
|
+
bytes
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
# GIF-spec LZW decode. Variable-width codes starting at
|
|
154
|
+
# min_code_size + 1 bits. Clear code = 2**min, end code =
|
|
155
|
+
# 2**min + 1.
|
|
156
|
+
def lzw_decode(compressed, min_code_size)
|
|
157
|
+
clear_code = 1 << min_code_size
|
|
158
|
+
end_code = clear_code + 1
|
|
159
|
+
codes = unpack_lzw_codes(compressed, min_code_size + 1, clear_code)
|
|
160
|
+
return nil unless codes
|
|
161
|
+
|
|
162
|
+
out = +"".b
|
|
163
|
+
dict = init_dictionary(min_code_size)
|
|
164
|
+
prev = nil
|
|
165
|
+
codes.each do |code|
|
|
166
|
+
if code == clear_code
|
|
167
|
+
dict = init_dictionary(min_code_size)
|
|
168
|
+
prev = nil
|
|
169
|
+
next
|
|
170
|
+
end
|
|
171
|
+
break if code == end_code
|
|
172
|
+
|
|
173
|
+
entry =
|
|
174
|
+
if dict.key?(code)
|
|
175
|
+
dict[code]
|
|
176
|
+
elsif prev && dict.key?(prev)
|
|
177
|
+
dict[prev] + first_byte(dict[prev])
|
|
178
|
+
else
|
|
179
|
+
return nil
|
|
180
|
+
end
|
|
181
|
+
out << entry
|
|
182
|
+
if prev && !dict.key?(code) && dict.length < 4096
|
|
183
|
+
dict[code] = dict[prev] + first_byte(entry)
|
|
184
|
+
end
|
|
185
|
+
prev = code
|
|
186
|
+
end
|
|
187
|
+
out
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def first_byte(str)
|
|
191
|
+
str.getbyte(0)&.chr(Encoding::BINARY) || "".b
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def init_dictionary(min_code_size)
|
|
195
|
+
clear_code = 1 << min_code_size
|
|
196
|
+
end_code = clear_code + 1
|
|
197
|
+
dict = {}
|
|
198
|
+
(0...clear_code).each { |i| dict[i] = first_byte_of_code(i) }
|
|
199
|
+
dict[clear_code] = ""
|
|
200
|
+
dict[end_code] = ""
|
|
201
|
+
dict
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def first_byte_of_code(i)
|
|
205
|
+
i.chr(Encoding::BINARY)
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Unpack variable-width codes from the LZW-compressed byte
|
|
209
|
+
# stream. GIF codes are packed LSB-first. Width starts at
|
|
210
|
+
# initial_width and grows by 1 when the next-code threshold
|
|
211
|
+
# reaches 2**width. Width caps at 12 bits.
|
|
212
|
+
def unpack_lzw_codes(bytes, initial_width, clear_code)
|
|
213
|
+
codes = []
|
|
214
|
+
bit_buffer = 0
|
|
215
|
+
bits_in_buffer = 0
|
|
216
|
+
width = initial_width
|
|
217
|
+
min_code_size = initial_width - 1
|
|
218
|
+
next_code = (1 << min_code_size) + 2 # next free dict slot
|
|
219
|
+
byte_index = 0
|
|
220
|
+
|
|
221
|
+
while byte_index < bytes.bytesize
|
|
222
|
+
bit_buffer |= (bytes.getbyte(byte_index) << bits_in_buffer)
|
|
223
|
+
bits_in_buffer += 8
|
|
224
|
+
byte_index += 1
|
|
225
|
+
|
|
226
|
+
while bits_in_buffer >= width
|
|
227
|
+
code = bit_buffer & ((1 << width) - 1)
|
|
228
|
+
bit_buffer >>= width
|
|
229
|
+
bits_in_buffer -= width
|
|
230
|
+
codes << code
|
|
231
|
+
if code == clear_code
|
|
232
|
+
width = initial_width
|
|
233
|
+
next_code = (1 << min_code_size) + 2
|
|
234
|
+
else
|
|
235
|
+
next_code += 1
|
|
236
|
+
if width < 12 && next_code > (1 << width)
|
|
237
|
+
width += 1
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
codes
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|
|
246
|
+
end
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "zlib"
|
|
4
|
+
|
|
5
|
+
module Pdfrb
|
|
6
|
+
module ImageLoader
|
|
7
|
+
# TIFF image loader. Pure-Ruby support for:
|
|
8
|
+
#
|
|
9
|
+
# * Format detection via the II/MM byte-order magic.
|
|
10
|
+
# * Header + IFD parsing to extract width, height, bpc, sample
|
|
11
|
+
# count, photometric interpretation, compression, strip
|
|
12
|
+
# offsets + byte counts.
|
|
13
|
+
# * Single-strip, uncompressed (compression == 1) RGB or
|
|
14
|
+
# grayscale pixel decode into raw image bytes suitable for
|
|
15
|
+
# embedding as a /FlateDecode image XObject.
|
|
16
|
+
#
|
|
17
|
+
# For multi-strip, tiled, YCbCr, or compressed TIFFs (LZW, PackBits,
|
|
18
|
+
# CCITT, JPEG-in-TIFF), the loader emits a metadata-only stub
|
|
19
|
+
# XObject. Callers should pre-convert those to PNG/JPEG.
|
|
20
|
+
module TIFF
|
|
21
|
+
COMPRESSION_NONE = 1
|
|
22
|
+
COMPRESSION_CCITT_RLE = 2
|
|
23
|
+
COMPRESSION_CCITT_FAX3 = 3
|
|
24
|
+
COMPRESSION_CCITT_FAX4 = 4
|
|
25
|
+
COMPRESSION_LZW = 5
|
|
26
|
+
COMPRESSION_OJPEG = 6
|
|
27
|
+
COMPRESSION_JPEG = 7
|
|
28
|
+
COMPRESSION_DEFLATE = 8
|
|
29
|
+
COMPRESSION_PACKBITS = 32773
|
|
30
|
+
|
|
31
|
+
module_function
|
|
32
|
+
|
|
33
|
+
def call(document, data, **_opts)
|
|
34
|
+
data = data.read if data.is_a?(IO) || data.is_a?(StringIO)
|
|
35
|
+
info = parse_header(data)
|
|
36
|
+
return nil if info.empty?
|
|
37
|
+
|
|
38
|
+
pixels = decode_pixels(data, info)
|
|
39
|
+
if pixels
|
|
40
|
+
build_compressed_image(document, info, pixels)
|
|
41
|
+
else
|
|
42
|
+
build_stub_image(document, info)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
# Parse the TIFF header (8 bytes) + first IFD. Returns a Hash
|
|
47
|
+
# with width, height, bits_per_sample, samples_per_pixel,
|
|
48
|
+
# photometric, compression, strip_offsets, strip_byte_counts,
|
|
49
|
+
# rows_per_strip, color_space, or empty Hash if not TIFF.
|
|
50
|
+
def parse_header(data)
|
|
51
|
+
return {} unless data.is_a?(::String) && data.bytesize >= 8
|
|
52
|
+
|
|
53
|
+
byte_order = data.byteslice(0, 2)
|
|
54
|
+
case byte_order
|
|
55
|
+
when "II" then little_endian = true
|
|
56
|
+
when "MM" then little_endian = false
|
|
57
|
+
else return {}
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
magic = read_u16(data, 2, little_endian)
|
|
61
|
+
return {} unless [42, 43].include?(magic)
|
|
62
|
+
return {} unless magic == 42
|
|
63
|
+
|
|
64
|
+
ifd_offset = read_u32(data, 4, little_endian)
|
|
65
|
+
parse_ifd(data, ifd_offset, little_endian)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def parse_ifd(data, offset, little_endian)
|
|
69
|
+
return {} if offset.nil? || offset + 2 > data.bytesize
|
|
70
|
+
|
|
71
|
+
count = read_u16(data, offset, little_endian)
|
|
72
|
+
entries = {}
|
|
73
|
+
count.times do |i|
|
|
74
|
+
entry_off = offset + 2 + (i * 12)
|
|
75
|
+
break if entry_off + 12 > data.bytesize
|
|
76
|
+
|
|
77
|
+
tag = read_u16(data, entry_off, little_endian)
|
|
78
|
+
type_id = read_u16(data, entry_off + 2, little_endian)
|
|
79
|
+
count_val = read_u32(data, entry_off + 4, little_endian)
|
|
80
|
+
value = read_ifd_value(data, entry_off + 8, type_id, count_val, little_endian)
|
|
81
|
+
entries[tag] = { type: type_id, count: count_val, value: value }
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
interpret_ifd(entries)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Decode pixel data when the TIFF is single-strip uncompressed
|
|
88
|
+
# RGB or grayscale. Returns binary pixel bytes or nil if the
|
|
89
|
+
# compression/photometric combo isn't supported here.
|
|
90
|
+
def decode_pixels(data, info)
|
|
91
|
+
return nil unless info[:compression] == COMPRESSION_NONE
|
|
92
|
+
return nil unless info[:strip_offsets] && info[:strip_byte_counts]
|
|
93
|
+
|
|
94
|
+
# Only handle 8-bit single-sample (gray) or 3-sample (RGB)
|
|
95
|
+
# in photometric 0/1/2.
|
|
96
|
+
return nil unless info[:bits_per_sample] == 8
|
|
97
|
+
return nil unless [1, 3].include?(info[:samples_per_pixel])
|
|
98
|
+
return nil unless [0, 1, 2].include?(info[:photometric])
|
|
99
|
+
|
|
100
|
+
bytes = read_strips(data, info)
|
|
101
|
+
return nil unless bytes
|
|
102
|
+
|
|
103
|
+
# TIFF rows are padded to word boundaries; PDF image XObjects
|
|
104
|
+
# don't need this padding. Strip per row.
|
|
105
|
+
channels = info[:samples_per_pixel]
|
|
106
|
+
row_bytes = info[:width] * channels
|
|
107
|
+
return nil if row_bytes.zero?
|
|
108
|
+
|
|
109
|
+
rows = info[:height]
|
|
110
|
+
out = +"".b
|
|
111
|
+
rows.times do |r|
|
|
112
|
+
offset = r * row_bytes
|
|
113
|
+
out << bytes.byteslice(offset, row_bytes)
|
|
114
|
+
end
|
|
115
|
+
out
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
# Read all strips concatenated. Single-strip case is most common
|
|
119
|
+
# for small images; multi-strip is supported when present.
|
|
120
|
+
def read_strips(data, info)
|
|
121
|
+
offsets = Array(info[:strip_offsets])
|
|
122
|
+
counts = Array(info[:strip_byte_counts])
|
|
123
|
+
return nil if offsets.length != counts.length
|
|
124
|
+
|
|
125
|
+
bytes = +"".b
|
|
126
|
+
offsets.each_with_index do |off, i|
|
|
127
|
+
n = counts[i]
|
|
128
|
+
piece = data.byteslice(off, n)
|
|
129
|
+
return nil unless piece && piece.bytesize == n
|
|
130
|
+
|
|
131
|
+
bytes << piece
|
|
132
|
+
end
|
|
133
|
+
bytes
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
def build_compressed_image(document, info, pixels)
|
|
137
|
+
compressed = Zlib.deflate(pixels)
|
|
138
|
+
image = document.add(
|
|
139
|
+
{
|
|
140
|
+
Type: :XObject, Subtype: :Image,
|
|
141
|
+
Width: info[:width], Height: info[:height],
|
|
142
|
+
BitsPerComponent: 8,
|
|
143
|
+
ColorSpace: info[:color_space],
|
|
144
|
+
Filter: :FlateDecode,
|
|
145
|
+
Length: compressed.bytesize
|
|
146
|
+
},
|
|
147
|
+
type: Pdfrb::Model::Type::XObjectImage
|
|
148
|
+
)
|
|
149
|
+
image.stream = compressed
|
|
150
|
+
image
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def build_stub_image(document, info)
|
|
154
|
+
image = document.add(
|
|
155
|
+
{
|
|
156
|
+
Type: :XObject, Subtype: :Image,
|
|
157
|
+
Width: info[:width], Height: info[:height],
|
|
158
|
+
BitsPerComponent: info[:bits_per_sample] || 8,
|
|
159
|
+
ColorSpace: info[:color_space] || :DeviceRGB,
|
|
160
|
+
Length: 0
|
|
161
|
+
},
|
|
162
|
+
type: Pdfrb::Model::Type::XObjectImage
|
|
163
|
+
)
|
|
164
|
+
image.stream = +""
|
|
165
|
+
image
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def read_ifd_value(data, offset, type_id, count, little_endian)
|
|
169
|
+
# For multi-value entries (count > 1 for SHORT/LONG), the
|
|
170
|
+
# value field is a pointer to the actual data. For count <= 1
|
|
171
|
+
# the value lives inline in the 4-byte value field.
|
|
172
|
+
case type_id
|
|
173
|
+
when 3 # SHORT
|
|
174
|
+
count <= 1 ? read_u16(data, offset, little_endian) : read_short_array(data, offset, count, little_endian)
|
|
175
|
+
when 4 # LONG
|
|
176
|
+
count <= 1 ? read_u32(data, offset, little_endian) : read_long_array(data, offset, count, little_endian)
|
|
177
|
+
else
|
|
178
|
+
count <= 1 ? read_u16(data, offset, little_endian) : read_u32(data, offset, little_endian)
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def read_short_array(data, value_field_offset, count, little_endian)
|
|
183
|
+
# If count * 2 <= 4, values live in the value field; otherwise
|
|
184
|
+
# the field holds a pointer.
|
|
185
|
+
if count * 2 <= 4
|
|
186
|
+
(0...count).map { |i| read_u16(data, value_field_offset + (i * 2), little_endian) }
|
|
187
|
+
else
|
|
188
|
+
ptr = read_u32(data, value_field_offset, little_endian)
|
|
189
|
+
(0...count).map { |i| read_u16(data, ptr + (i * 2), little_endian) }
|
|
190
|
+
end
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def read_long_array(data, value_field_offset, count, little_endian)
|
|
194
|
+
if count * 4 <= 4
|
|
195
|
+
(0...count).map { |i| read_u32(data, value_field_offset + (i * 4), little_endian) }
|
|
196
|
+
else
|
|
197
|
+
ptr = read_u32(data, value_field_offset, little_endian)
|
|
198
|
+
(0...count).map { |i| read_u32(data, ptr + (i * 4), little_endian) }
|
|
199
|
+
end
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def read_u16(data, offset, little_endian)
|
|
203
|
+
bytes = data.bytes[offset, 2]
|
|
204
|
+
return 0 unless bytes && bytes.length == 2
|
|
205
|
+
|
|
206
|
+
little_endian ? ((bytes[1] << 8) | bytes[0]) : ((bytes[0] << 8) | bytes[1])
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def read_u32(data, offset, little_endian)
|
|
210
|
+
bytes = data.bytes[offset, 4]
|
|
211
|
+
return 0 unless bytes && bytes.length == 4
|
|
212
|
+
|
|
213
|
+
if little_endian
|
|
214
|
+
(bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0]
|
|
215
|
+
else
|
|
216
|
+
(bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3]
|
|
217
|
+
end
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def interpret_ifd(entries)
|
|
221
|
+
width = entries[256]&.[](:value)
|
|
222
|
+
height = entries[257]&.[](:value)
|
|
223
|
+
bps = entries[258]&.[](:value) || 8
|
|
224
|
+
bps = bps.first if bps.is_a?(::Array)
|
|
225
|
+
spp = entries[277]&.[](:value) || 1
|
|
226
|
+
spp = spp.first if spp.is_a?(::Array)
|
|
227
|
+
photometric = entries[262]&.[](:value) || 1
|
|
228
|
+
compression = entries[259]&.[](:value) || COMPRESSION_NONE
|
|
229
|
+
strip_offsets = entries[273]&.[](:value)
|
|
230
|
+
strip_byte_counts = entries[279]&.[](:value)
|
|
231
|
+
rows_per_strip = entries[278]&.[](:value) || height
|
|
232
|
+
{
|
|
233
|
+
width: width,
|
|
234
|
+
height: height,
|
|
235
|
+
bits_per_sample: bps,
|
|
236
|
+
samples_per_pixel: spp,
|
|
237
|
+
photometric: photometric,
|
|
238
|
+
compression: compression,
|
|
239
|
+
strip_offsets: strip_offsets,
|
|
240
|
+
strip_byte_counts: strip_byte_counts,
|
|
241
|
+
rows_per_strip: rows_per_strip,
|
|
242
|
+
color_space: color_space_for_photometric(photometric),
|
|
243
|
+
}
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
def color_space_for_photometric(photometric)
|
|
247
|
+
case photometric
|
|
248
|
+
when 0, 1 then :DeviceGray
|
|
249
|
+
when 5 then :DeviceCMYK
|
|
250
|
+
# Default covers RGB (2), unknown photometric interpretations,
|
|
251
|
+
# and nil photometric (which we treat as RGB by convention).
|
|
252
|
+
else :DeviceRGB
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
end
|
data/lib/pdfrb/image_loader.rb
CHANGED