Class: Pura::Tiff::Decoder

Inherits:
Object
  • Object
show all
Defined in:
lib/pura/tiff/decoder.rb

Constant Summary collapse

TAG_IMAGE_WIDTH =

TIFF tag IDs

256
TAG_IMAGE_LENGTH =
257
TAG_BITS_PER_SAMPLE =
258
TAG_COMPRESSION =
259
TAG_PHOTOMETRIC =
262
TAG_STRIP_OFFSETS =
273
TAG_SAMPLES_PER_PIXEL =
277
TAG_ROWS_PER_STRIP =
278
TAG_STRIP_BYTE_COUNTS =
279
TAG_X_RESOLUTION =
282
TAG_Y_RESOLUTION =
283
TAG_RESOLUTION_UNIT =
296
TAG_COLOR_MAP =
320
TAG_EXTRA_SAMPLES =
338
TAG_SAMPLE_FORMAT =
339
COMPRESSION_NONE =

Compression types

1
COMPRESSION_LZW =
5
COMPRESSION_PACKBITS =
32_773
PHOTO_MIN_IS_WHITE =

Photometric interpretation

0
PHOTO_MIN_IS_BLACK =
1
PHOTO_RGB =
2
PHOTO_PALETTE =
3
TYPE_BYTE =

TIFF field types

1
TYPE_ASCII =
2
TYPE_SHORT =
3
TYPE_LONG =
4
TYPE_RATIONAL =
5
TYPE_SIZES =
{
  TYPE_BYTE => 1,
  TYPE_ASCII => 1,
  TYPE_SHORT => 2,
  TYPE_LONG => 4,
  TYPE_RATIONAL => 8
}.freeze
DEFAULT_MAX_INPUT_BYTES =
64 * 1024 * 1024
DEFAULT_MAX_PIXELS =
40_000_000
DEFAULT_MAX_DECODED_BYTES =
256 * 1024 * 1024

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(data, max_pixels: DEFAULT_MAX_PIXELS, max_decoded_bytes: DEFAULT_MAX_DECODED_BYTES) ⇒ Decoder



71
72
73
74
75
76
77
78
79
80
81
# File 'lib/pura/tiff/decoder.rb', line 71

def initialize(data, max_pixels: DEFAULT_MAX_PIXELS, max_decoded_bytes: DEFAULT_MAX_DECODED_BYTES)
  unless [max_pixels, max_decoded_bytes].all? { |value| value.is_a?(Integer) && value.positive? }
    raise ArgumentError, "decode limits must be positive integers"
  end

  @max_pixels = max_pixels
  @max_decoded_bytes = max_decoded_bytes
  @data = data
  @pos = 0
  @little = true
end

Class Method Details

.decode(input, max_input_bytes: DEFAULT_MAX_INPUT_BYTES, **options) ⇒ Object

Raises:



56
57
58
59
60
61
62
63
64
65
66
67
68
69
# File 'lib/pura/tiff/decoder.rb', line 56

def self.decode(input, max_input_bytes: DEFAULT_MAX_INPUT_BYTES, **options)
  unless max_input_bytes.is_a?(Integer) && max_input_bytes.positive?
    raise ArgumentError, "max_input_bytes must be a positive integer"
  end

  data = if input.is_a?(String) && input.bytesize < 4096 && !input.include?("\0") && File.exist?(input)
           File.binread(input, max_input_bytes + 1)
         else
           input.b
         end
  raise LimitExceeded, "TIFF input limit exceeded" if data.bytesize > max_input_bytes

  new(data, **options).decode
end

Instance Method Details

#decodeObject

Raises:



83
84
85
86
87
88
89
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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
# File 'lib/pura/tiff/decoder.rb', line 83

def decode
  parse_header
  tags = parse_ifd(@ifd_offset)

  width = get_tag_value(tags, TAG_IMAGE_WIDTH) or raise DecodeError, "missing ImageWidth tag"
  height = get_tag_value(tags, TAG_IMAGE_LENGTH) or raise DecodeError, "missing ImageLength tag"
  compression = get_tag_value(tags, TAG_COMPRESSION) || COMPRESSION_NONE
  photometric = get_tag_value(tags, TAG_PHOTOMETRIC) || PHOTO_MIN_IS_BLACK
  samples_per_pixel = get_tag_value(tags, TAG_SAMPLES_PER_PIXEL) || 1
  bits_per_sample = get_tag_values(tags, TAG_BITS_PER_SAMPLE) || [8]
  get_tag_value(tags, TAG_ROWS_PER_STRIP) || height
  strip_offsets = get_tag_values(tags, TAG_STRIP_OFFSETS) or raise DecodeError, "missing StripOffsets tag"
  strip_byte_counts = get_tag_values(tags,
                                     TAG_STRIP_BYTE_COUNTS) or raise DecodeError, "missing StripByteCounts tag"
  color_map = get_tag_values(tags, TAG_COLOR_MAP)
  extra_samples = get_tag_values(tags, TAG_EXTRA_SAMPLES)

  unless bits_per_sample.all? { |b| b == 8 }
    raise DecodeError, "only 8-bit samples supported, got #{bits_per_sample.inspect}"
  end

  unless [COMPRESSION_NONE, COMPRESSION_LZW, COMPRESSION_PACKBITS].include?(compression)
    raise DecodeError, "unsupported compression: #{compression}"
  end

  raise DecodeError, "invalid TIFF dimensions" unless width.positive? && height.positive?
  raise LimitExceeded, "TIFF pixel limit exceeded" if width * height > @max_pixels
  raise DecodeError, "invalid SamplesPerPixel" unless samples_per_pixel.positive?

  expected = width * height * samples_per_pixel
  if [expected, width * height * 3].max > @max_decoded_bytes
    raise LimitExceeded, "TIFF decoded byte limit exceeded"
  end
  unless strip_offsets.size == strip_byte_counts.size
    raise DecodeError, "strip offsets and byte counts must have equal sizes"
  end

  # Decompress all strips, bounding every append before allocating output.
  raw = String.new(encoding: Encoding::BINARY)
  strip_offsets.each_with_index do |offset, i|
    count = strip_byte_counts[i]
    strip_data = @data.byteslice(offset, count)
    raise DecodeError, "truncated strip data" unless strip_data && strip_data.bytesize == count

    remaining = expected - raw.bytesize
    decoded = case compression
              when COMPRESSION_NONE then strip_data
              when COMPRESSION_LZW then decompress_lzw(strip_data, remaining)
              when COMPRESSION_PACKBITS then decompress_packbits(strip_data, remaining)
              end
    raise DecodeError, "excess TIFF strip data size" if decoded.bytesize > remaining

    raw << decoded
  end
  raise DecodeError, "incorrect TIFF strip data size" unless raw.bytesize == expected

  # Convert to RGB
  pixels = convert_to_rgb(raw, width, height, photometric, samples_per_pixel, color_map, extra_samples)
  Image.new(width, height, pixels)
end