Module: Pdfrb::ImageLoader::TIFF
- Defined in:
- lib/pdfrb/image_loader/tiff.rb
Overview
TIFF image loader. Pure-Ruby support for:
* Format detection via the II/MM byte-order magic.
* Header + IFD parsing to extract width, height, bpc, sample
count, photometric interpretation, compression, strip
offsets + byte counts.
* Single-strip, uncompressed (compression == 1) RGB or
grayscale pixel decode into raw image bytes suitable for
embedding as a /FlateDecode image XObject.
For multi-strip, tiled, YCbCr, or compressed TIFFs (LZW, PackBits, CCITT, JPEG-in-TIFF), the loader emits a metadata-only stub XObject. Callers should pre-convert those to PNG/JPEG.
Constant Summary collapse
- COMPRESSION_NONE =
1- COMPRESSION_CCITT_RLE =
2- COMPRESSION_CCITT_FAX3 =
3- COMPRESSION_CCITT_FAX4 =
4- COMPRESSION_LZW =
5- COMPRESSION_OJPEG =
6- COMPRESSION_JPEG =
7- COMPRESSION_DEFLATE =
8- COMPRESSION_PACKBITS =
32773
Class Method Summary collapse
- .build_compressed_image(document, info, pixels) ⇒ Object
- .build_stub_image(document, info) ⇒ Object
- .call(document, data, **_opts) ⇒ Object
- .color_space_for_photometric(photometric) ⇒ Object
-
.decode_pixels(data, info) ⇒ Object
Decode pixel data when the TIFF is single-strip uncompressed RGB or grayscale.
- .interpret_ifd(entries) ⇒ Object
-
.parse_header(data) ⇒ Object
Parse the TIFF header (8 bytes) + first IFD.
- .parse_ifd(data, offset, little_endian) ⇒ Object
- .read_ifd_value(data, offset, type_id, count, little_endian) ⇒ Object
- .read_long_array(data, value_field_offset, count, little_endian) ⇒ Object
- .read_short_array(data, value_field_offset, count, little_endian) ⇒ Object
-
.read_strips(data, info) ⇒ Object
Read all strips concatenated.
- .read_u16(data, offset, little_endian) ⇒ Object
- .read_u32(data, offset, little_endian) ⇒ Object
Class Method Details
.build_compressed_image(document, info, pixels) ⇒ Object
136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 136 def build_compressed_image(document, info, pixels) compressed = Zlib.deflate(pixels) image = document.add( { Type: :XObject, Subtype: :Image, Width: info[:width], Height: info[:height], BitsPerComponent: 8, ColorSpace: info[:color_space], Filter: :FlateDecode, Length: compressed.bytesize }, type: Pdfrb::Model::Type::XObjectImage ) image.stream = compressed image end |
.build_stub_image(document, info) ⇒ Object
153 154 155 156 157 158 159 160 161 162 163 164 165 166 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 153 def build_stub_image(document, info) image = document.add( { Type: :XObject, Subtype: :Image, Width: info[:width], Height: info[:height], BitsPerComponent: info[:bits_per_sample] || 8, ColorSpace: info[:color_space] || :DeviceRGB, Length: 0 }, type: Pdfrb::Model::Type::XObjectImage ) image.stream = +"" image end |
.call(document, data, **_opts) ⇒ Object
33 34 35 36 37 38 39 40 41 42 43 44 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 33 def call(document, data, **_opts) data = data.read if data.is_a?(IO) || data.is_a?(StringIO) info = parse_header(data) return nil if info.empty? pixels = decode_pixels(data, info) if pixels build_compressed_image(document, info, pixels) else build_stub_image(document, info) end end |
.color_space_for_photometric(photometric) ⇒ Object
246 247 248 249 250 251 252 253 254 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 246 def color_space_for_photometric(photometric) case photometric when 0, 1 then :DeviceGray when 5 then :DeviceCMYK # Default covers RGB (2), unknown photometric interpretations, # and nil photometric (which we treat as RGB by convention). else :DeviceRGB end end |
.decode_pixels(data, info) ⇒ Object
Decode pixel data when the TIFF is single-strip uncompressed RGB or grayscale. Returns binary pixel bytes or nil if the compression/photometric combo isn't supported here.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 90 def decode_pixels(data, info) return nil unless info[:compression] == COMPRESSION_NONE return nil unless info[:strip_offsets] && info[:strip_byte_counts] # Only handle 8-bit single-sample (gray) or 3-sample (RGB) # in photometric 0/1/2. return nil unless info[:bits_per_sample] == 8 return nil unless [1, 3].include?(info[:samples_per_pixel]) return nil unless [0, 1, 2].include?(info[:photometric]) bytes = read_strips(data, info) return nil unless bytes # TIFF rows are padded to word boundaries; PDF image XObjects # don't need this padding. Strip per row. channels = info[:samples_per_pixel] row_bytes = info[:width] * channels return nil if row_bytes.zero? rows = info[:height] out = +"".b rows.times do |r| offset = r * row_bytes out << bytes.byteslice(offset, row_bytes) end out end |
.interpret_ifd(entries) ⇒ Object
220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 220 def interpret_ifd(entries) width = entries[256]&.[](:value) height = entries[257]&.[](:value) bps = entries[258]&.[](:value) || 8 bps = bps.first if bps.is_a?(::Array) spp = entries[277]&.[](:value) || 1 spp = spp.first if spp.is_a?(::Array) photometric = entries[262]&.[](:value) || 1 compression = entries[259]&.[](:value) || COMPRESSION_NONE strip_offsets = entries[273]&.[](:value) strip_byte_counts = entries[279]&.[](:value) rows_per_strip = entries[278]&.[](:value) || height { width: width, height: height, bits_per_sample: bps, samples_per_pixel: spp, photometric: photometric, compression: compression, strip_offsets: strip_offsets, strip_byte_counts: strip_byte_counts, rows_per_strip: rows_per_strip, color_space: color_space_for_photometric(photometric), } end |
.parse_header(data) ⇒ Object
Parse the TIFF header (8 bytes) + first IFD. Returns a Hash with width, height, bits_per_sample, samples_per_pixel, photometric, compression, strip_offsets, strip_byte_counts, rows_per_strip, color_space, or empty Hash if not TIFF.
50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 50 def parse_header(data) return {} unless data.is_a?(::String) && data.bytesize >= 8 byte_order = data.byteslice(0, 2) case byte_order when "II" then little_endian = true when "MM" then little_endian = false else return {} end magic = read_u16(data, 2, little_endian) return {} unless [42, 43].include?(magic) return {} unless magic == 42 ifd_offset = read_u32(data, 4, little_endian) parse_ifd(data, ifd_offset, little_endian) end |
.parse_ifd(data, offset, little_endian) ⇒ Object
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 68 def parse_ifd(data, offset, little_endian) return {} if offset.nil? || offset + 2 > data.bytesize count = read_u16(data, offset, little_endian) entries = {} count.times do |i| entry_off = offset + 2 + (i * 12) break if entry_off + 12 > data.bytesize tag = read_u16(data, entry_off, little_endian) type_id = read_u16(data, entry_off + 2, little_endian) count_val = read_u32(data, entry_off + 4, little_endian) value = read_ifd_value(data, entry_off + 8, type_id, count_val, little_endian) entries[tag] = { type: type_id, count: count_val, value: value } end interpret_ifd(entries) end |
.read_ifd_value(data, offset, type_id, count, little_endian) ⇒ Object
168 169 170 171 172 173 174 175 176 177 178 179 180 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 168 def read_ifd_value(data, offset, type_id, count, little_endian) # For multi-value entries (count > 1 for SHORT/LONG), the # value field is a pointer to the actual data. For count <= 1 # the value lives inline in the 4-byte value field. case type_id when 3 # SHORT count <= 1 ? read_u16(data, offset, little_endian) : read_short_array(data, offset, count, little_endian) when 4 # LONG count <= 1 ? read_u32(data, offset, little_endian) : read_long_array(data, offset, count, little_endian) else count <= 1 ? read_u16(data, offset, little_endian) : read_u32(data, offset, little_endian) end end |
.read_long_array(data, value_field_offset, count, little_endian) ⇒ Object
193 194 195 196 197 198 199 200 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 193 def read_long_array(data, value_field_offset, count, little_endian) if count * 4 <= 4 (0...count).map { |i| read_u32(data, value_field_offset + (i * 4), little_endian) } else ptr = read_u32(data, value_field_offset, little_endian) (0...count).map { |i| read_u32(data, ptr + (i * 4), little_endian) } end end |
.read_short_array(data, value_field_offset, count, little_endian) ⇒ Object
182 183 184 185 186 187 188 189 190 191 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 182 def read_short_array(data, value_field_offset, count, little_endian) # If count * 2 <= 4, values live in the value field; otherwise # the field holds a pointer. if count * 2 <= 4 (0...count).map { |i| read_u16(data, value_field_offset + (i * 2), little_endian) } else ptr = read_u32(data, value_field_offset, little_endian) (0...count).map { |i| read_u16(data, ptr + (i * 2), little_endian) } end end |
.read_strips(data, info) ⇒ Object
Read all strips concatenated. Single-strip case is most common for small images; multi-strip is supported when present.
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 120 def read_strips(data, info) offsets = Array(info[:strip_offsets]) counts = Array(info[:strip_byte_counts]) return nil if offsets.length != counts.length bytes = +"".b offsets.each_with_index do |off, i| n = counts[i] piece = data.byteslice(off, n) return nil unless piece && piece.bytesize == n bytes << piece end bytes end |
.read_u16(data, offset, little_endian) ⇒ Object
202 203 204 205 206 207 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 202 def read_u16(data, offset, little_endian) bytes = data.bytes[offset, 2] return 0 unless bytes && bytes.length == 2 little_endian ? ((bytes[1] << 8) | bytes[0]) : ((bytes[0] << 8) | bytes[1]) end |
.read_u32(data, offset, little_endian) ⇒ Object
209 210 211 212 213 214 215 216 217 218 |
# File 'lib/pdfrb/image_loader/tiff.rb', line 209 def read_u32(data, offset, little_endian) bytes = data.bytes[offset, 4] return 0 unless bytes && bytes.length == 4 if little_endian (bytes[3] << 24) | (bytes[2] << 16) | (bytes[1] << 8) | bytes[0] else (bytes[0] << 24) | (bytes[1] << 16) | (bytes[2] << 8) | bytes[3] end end |