# Copyright (c) 2007, 2008, 2009 - R.W. van 't Veer require 'rational' module EXIFR # = TIFF decoder # # == Date properties # The properties :date_time, :date_time_original, # :date_time_digitized coerced into Time objects. # # == Orientation # The property :orientation describes the subject rotated and/or # mirrored in relation to the camera. It is translated to one of the following # instances: # * TopLeftOrientation # * TopRightOrientation # * BottomRightOrientation # * BottomLeftOrientation # * LeftTopOrientation # * RightTopOrientation # * RightBottomOrientation # * LeftBottomOrientation # # These instances of Orientation have two methods: # * to_i; return the original integer # * transform_rmagick(image); transforms the given RMagick::Image # to a viewable version # # == Examples # EXIFR::TIFF.new('DSC_0218.TIF').width # => 3008 # EXIFR::TIFF.new('DSC_0218.TIF')[1].width # => 160 # EXIFR::TIFF.new('DSC_0218.TIF').model # => "NIKON D1X" # EXIFR::TIFF.new('DSC_0218.TIF').date_time # => Tue May 23 19:15:32 +0200 2006 # EXIFR::TIFF.new('DSC_0218.TIF').exposure_time # => Rational(1, 100) # EXIFR::TIFF.new('DSC_0218.TIF').orientation # => EXIFR::TIFF::Orientation class TIFF include Enumerable # JPEG thumbnails attr_reader :jpeg_thumbnails TAG_MAPPING = {} # :nodoc: TAG_MAPPING.merge!({ :image => { 0x00FE => :new_subfile_type, 0x00FF => :subfile_type, 0x0100 => :image_width, 0x0101 => :image_length, 0x0102 => :bits_per_sample, 0x0103 => :compression, 0x0106 => :photometric_interpretation, 0x0107 => :threshholding, 0x0108 => :cell_width, 0x0109 => :cell_length, 0x010a => :fill_order, 0x010d => :document_name, 0x010e => :image_description, 0x010f => :make, 0x0110 => :model, 0x0111 => :strip_offsets, 0x0112 => :orientation, 0x0115 => :samples_per_pixel, 0x0116 => :rows_per_strip, 0x0117 => :strip_byte_counts, 0x0118 => :min_sample_value, 0x0119 => :max_sample_value, 0x011a => :x_resolution, 0x011b => :y_resolution, 0x011c => :planar_configuration, 0x011d => :page_name, 0x011e => :x_position, 0x011f => :y_position, 0x0120 => :free_offsets, 0x0121 => :free_byte_counts, 0x0122 => :gray_response_unit, 0x0123 => :gray_response_curve, 0x0124 => :t4_options, 0x0125 => :t6_options, 0x0128 => :resolution_unit, 0x012d => :transfer_function, 0x0131 => :software, 0x0132 => :date_time, 0x013b => :artist, 0x013c => :host_computer, 0x013a => :predictor, 0x013e => :white_point, 0x013f => :primary_chromaticities, 0x0140 => :color_map, 0x0141 => :halftone_hints, 0x0142 => :tile_width, 0x0143 => :tile_length, 0x0144 => :tile_offsets, 0x0145 => :tile_byte_counts, 0x0146 => :bad_fax_lines, 0x0147 => :clean_fax_data, 0x0148 => :consecutive_bad_fax_lines, 0x014a => :sub_ifds, 0x014c => :ink_set, 0x014d => :ink_names, 0x014e => :number_of_inks, 0x0150 => :dot_range, 0x0151 => :target_printer, 0x0152 => :extra_samples, 0x0156 => :transfer_range, 0x0157 => :clip_path, 0x0158 => :x_clip_path_units, 0x0159 => :y_clip_path_units, 0x015a => :indexed, 0x015b => :jpeg_tables, 0x015f => :opi_proxy, 0x0190 => :global_parameters_ifd, 0x0191 => :profile_type, 0x0192 => :fax_profile, 0x0193 => :coding_methods, 0x0194 => :version_year, 0x0195 => :mode_number, 0x01B1 => :decode, 0x01B2 => :default_image_color, 0x0200 => :jpegproc, 0x0201 => :jpeg_interchange_format, 0x0202 => :jpeg_interchange_format_length, 0x0203 => :jpeg_restart_interval, 0x0205 => :jpeg_lossless_predictors, 0x0206 => :jpeg_point_transforms, 0x0207 => :jpeg_q_tables, 0x0208 => :jpeg_dc_tables, 0x0209 => :jpeg_ac_tables, 0x0211 => :ycb_cr_coefficients, 0x0212 => :ycb_cr_sub_sampling, 0x0213 => :ycb_cr_positioning, 0x0214 => :reference_black_white, 0x022F => :strip_row_counts, 0x02BC => :xmp, 0x800D => :image_id, 0x87AC => :image_layer, 0x8298 => :copyright, 0x83bb => :iptc, 0x8769 => :exif, 0x8825 => :gps, }, :exif => { 0x829a => :exposure_time, 0x829d => :f_number, 0x8822 => :exposure_program, 0x8824 => :spectral_sensitivity, 0x8827 => :iso_speed_ratings, 0x8828 => :oecf, 0x9000 => :exif_version, 0x9003 => :date_time_original, 0x9004 => :date_time_digitized, 0x9101 => :components_configuration, 0x9102 => :compressed_bits_per_pixel, 0x9201 => :shutter_speed_value, 0x9202 => :aperture_value, 0x9203 => :brightness_value, 0x9204 => :exposure_bias_value, 0x9205 => :max_aperture_value, 0x9206 => :subject_distance, 0x9207 => :metering_mode, 0x9208 => :light_source, 0x9209 => :flash, 0x920a => :focal_length, 0x9214 => :subject_area, 0x927c => :maker_note, 0x9286 => :user_comment, 0x9290 => :subsec_time, 0x9291 => :subsec_time_orginal, 0x9292 => :subsec_time_digitized, 0xa000 => :flashpix_version, 0xa001 => :color_space, 0xa002 => :pixel_x_dimension, 0xa003 => :pixel_y_dimension, 0xa004 => :related_sound_file, 0xa20b => :flash_energy, 0xa20c => :spatial_frequency_response, 0xa20e => :focal_plane_x_resolution, 0xa20f => :focal_plane_y_resolution, 0xa210 => :focal_plane_resolution_unit, 0xa214 => :subject_location, 0xa215 => :exposure_index, 0xa217 => :sensing_method, 0xa300 => :file_source, 0xa301 => :scene_type, 0xa302 => :cfa_pattern, 0xa401 => :custom_rendered, 0xa402 => :exposure_mode, 0xa403 => :white_balance, 0xa404 => :digital_zoom_ratio, 0xa405 => :focal_length_in_35mm_film, 0xa406 => :scene_capture_type, 0xa407 => :gain_control, 0xa408 => :contrast, 0xa409 => :saturation, 0xa40a => :sharpness, 0xa40b => :device_setting_description, 0xa40c => :subject_distance_range, 0xa420 => :image_unique_id }, :gps => { 0x0000 => :gps_version_id, 0x0001 => :gps_latitude_ref, 0x0002 => :gps_latitude, 0x0003 => :gps_longitude_ref, 0x0004 => :gps_longitude, 0x0005 => :gps_altitude_ref, 0x0006 => :gps_altitude , 0x0007 => :gps_time_stamp, 0x0008 => :gps_satellites, 0x0009 => :gps_status, 0x000a => :gps_measure_mode, 0x000b => :gps_dop, 0x000c => :gps_speed_ref, 0x000d => :gps_speed, 0x000e => :gps_track_ref, 0x000f => :gps_track, 0x0010 => :gps_img_direction_ref, 0x0011 => :gps_img_direction, 0x0012 => :gps_map_datum, 0x0013 => :gps_dest_latitude_ref, 0x0014 => :gps_dest_latitude, 0x0015 => :gps_dest_longitude_ref, 0x0016 => :gps_dest_longitude, 0x0017 => :gps_dest_bearing_ref, 0x0018 => :gps_dest_bearing, 0x0019 => :gps_dest_distance_ref, 0x001a => :gps_dest_distance, 0x001b => :gps_processing_method, 0x001c => :gps_area_information, 0x001d => :gps_date_stamp, 0x001e => :gps_differential, }, }) IFD_TAGS = [:image, :exif, :gps] # :nodoc: time_proc = proc do |value| if value =~ /^(\d{4}):(\d\d):(\d\d) (\d\d):(\d\d):(\d\d)$/ Time.mktime($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i) rescue nil else value end end # The orientation of the image with respect to the rows and columns. class Orientation def initialize(value, type) # :nodoc: @value, @type = value, type end # Field value. def to_i @value end # Rotate and/or flip for proper viewing. def transform_rmagick(img) case @type when :TopRight ; img.flop when :BottomRight ; img.rotate(180) when :BottomLeft ; img.flip when :LeftTop ; img.rotate(90).flop when :RightTop ; img.rotate(90) when :RightBottom ; img.rotate(270).flop when :LeftBottom ; img.rotate(270) else img end end def ==(other) # :nodoc: Orientation === other && to_i == other.to_i end end ORIENTATIONS = [] # :nodoc: [ nil, :TopLeft, :TopRight, :BottomRight, :BottomLeft, :LeftTop, :RightTop, :RightBottom, :LeftBottom, ].each_with_index do |type,index| next unless type const_set("#{type}Orientation", ORIENTATIONS[index] = Orientation.new(index, type)) end ADAPTERS = Hash.new { proc { |v| v } } # :nodoc: ADAPTERS.merge!({ :date_time_original => time_proc, :date_time_digitized => time_proc, :date_time => time_proc, :orientation => proc { |v| ORIENTATIONS[v] } }) # Names for all recognized TIFF fields. TAGS = ([TAG_MAPPING.keys, TAG_MAPPING.values.map{|v|v.values}].flatten.uniq - IFD_TAGS).map{|v|v.to_s} # +file+ is a filename or an IO object. Hint: use StringIO when working with slurped data like blobs. def initialize(file) data = Data.new(file) @ifds = [IFD.new(data)] while ifd = @ifds.last.next break if @ifds.find{|i| i.offset == ifd.offset} @ifds << ifd end @jpeg_thumbnails = @ifds.map do |ifd| if ifd.jpeg_interchange_format && ifd.jpeg_interchange_format_length start, length = ifd.jpeg_interchange_format, ifd.jpeg_interchange_format_length data[start..(start + length)] end end.compact end # Number of images. def size @ifds.size end # Yield for each image. def each @ifds.each { |ifd| yield ifd } end # Get +index+ image. def [](index) index.is_a?(Symbol) ? to_hash[index] : @ifds[index] end # Dispatch to first image. def method_missing(method, *args) super unless args.empty? if @ifds.first.respond_to?(method) @ifds.first.send(method) elsif TAGS.include?(method.to_s) @ifds.first.to_hash[method] else super end end def respond_to?(method) # :nodoc: super || (@ifds && @ifds.first && @ifds.first.respond_to?(method)) || TAGS.include?(method.to_s) end def methods # :nodoc: (super + TAGS + IFD.instance_methods(false)).uniq end class << self alias instance_methods_without_tiff_extras instance_methods def instance_methods(include_super = true) # :nodoc: (instance_methods_without_tiff_extras(include_super) + TAGS + IFD.instance_methods(false)).uniq end end # Convenience method to access image width. def width; @ifds.first.width; end # Convenience method to access image height. def height; @ifds.first.height; end # Get a hash presentation of the (first) image. def to_hash; @ifds.first.to_hash; end def inspect # :nodoc: @ifds.inspect end class IFD # :nodoc: attr_reader :type, :fields, :offset def initialize(data, offset = nil, type = :image) @data, @offset, @type, @fields = data, offset, type, {} pos = offset || @data.readlong(4) num = @data.readshort(pos) pos += 2 num.times do add_field(Field.new(@data, pos)) pos += 12 end @offset_next = @data.readlong(pos) end def method_missing(method, *args) super unless args.empty? && TAGS.include?(method.to_s) to_hash[method] end def width; image_width; end def height; image_length; end def to_hash @hash ||= begin result = @fields.dup result.delete_if { |key,value| value.nil? } result.each do |key,value| if IFD_TAGS.include? key result.merge!(value.to_hash) result.delete key end end end end def inspect to_hash.inspect end def next? @offset_next != 0 && @offset_next < @data.size end def next IFD.new(@data, @offset_next) if next? end def to_yaml_properties ['@fields'] end private def add_field(field) return unless tag = TAG_MAPPING[@type][field.tag] return if @fields[tag] if IFD_TAGS.include? tag @fields[tag] = IFD.new(@data, field.offset, tag) else value = field.value.map { |v| ADAPTERS[tag][v] } if field.value @fields[tag] = value.kind_of?(Array) && value.size == 1 ? value.first : value end end end class Field # :nodoc: attr_reader :tag, :offset, :value def initialize(data, pos) @tag, count, @offset = data.readshort(pos), data.readlong(pos + 4), data.readlong(pos + 8) @type = data.readshort(pos + 2) case @type when 1, 6 # byte, signed byte # TODO handle signed bytes len, pack = count, proc { |d| d } when 2 # ascii len, pack = count, proc { |d| d.strip } when 3, 8 # short, signed short # TODO handle signed len, pack = count * 2, proc { |d| d.unpack(data.short + '*') } when 4, 9 # long, signed long # TODO handle signed len, pack = count * 4, proc { |d| d.unpack(data.long + '*') } when 7 # undefined # UserComment if @tag == 0x9286 len, pack = count, proc { |d| d.strip } len -= 8 # reduce to account for first 8 bytes start = len > 4 ? @offset + 8 : (pos + 8) # UserComment first 8-bytes is char code @value = [pack[data[start..(start + len - 1)]]].flatten end when 5, 10 len, pack = count * 8, proc do |d| r = [] d.unpack(data.long + '*').each_with_index do |v,i| i % 2 == 0 ? r << [v] : r.last << v end r.map do |f| if f[1] == 0 # allow NaN and Infinity f[0].to_f.quo(f[1]) else Rational.respond_to?(:reduce) ? Rational.reduce(*f) : f[0].quo(f[1]) end end end end if len && pack && @type != 7 start = len > 4 ? @offset : (pos + 8) d = data[start..(start + len - 1)] @value = d && [pack[d]].flatten end end end class Data #:nodoc: attr_reader :short, :long def initialize(file) @file = file.respond_to?(:read) ? file : File.open(file, 'rb') @buffer = '' @pos = 0 case self[0..1] when 'II'; @short, @long = 'v', 'V' when 'MM'; @short, @long = 'n', 'N' else; raise 'no II or MM marker found' end end def [](pos) unless pos.respond_to?(:begin) && pos.respond_to?(:end) pos = pos..pos end if pos.begin < @pos || pos.end >= @pos + @buffer.size read_for(pos) end @buffer && @buffer[(pos.begin - @pos)..(pos.end - @pos)] end def readshort(pos) self[pos..(pos + 1)].unpack(@short)[0] end def readlong(pos) self[pos..(pos + 3)].unpack(@long)[0] end def size @file.seek(0, IO::SEEK_END) @file.pos end private def read_for(pos) @file.seek(@pos = pos.begin) @buffer = @file.read([pos.end - pos.begin, 4096].max) end end end end