Raw File
dngmeta.rb
#!/usr/bin/env ruby

# MIT License
#
# Copyright (c) 2016 Roman Lebedev
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all
# copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.

if ARGV.size < 1
  $stderr.puts "Usage: dngmeta.rb <file1> [file2] ..."
  exit 2
end

def is_iso_ok(exifhash, tag)
  if not exifhash[tag] or exifhash[tag] == "0" or exifhash[tag] == "65535"
    return false
  else
    return exifhash[tag]
  end
end

def get_iso(exifhash)
  isokeys = [
    "Exif.Photo.ISOSpeedRatings", "Exif.Photo.RecommendedExposureIndex",
    "Exif.Photo.StandardOutputSensitivity", "Exif.Nikon3.ISOSpeed",
    "Exif.Nikon3.ISOSettings", "Exif.NikonIi.ISO"
  ]

  iso = nil

  isokeys.each do |key|
    value = is_iso_ok(exifhash, key)
    return value if value
  end

  return iso
end

def get_exif(filename)
  exifhash = {}

  IO.popen("exiv2 -q -Qm -Pkt \"#{filename}\" 2> /dev/null") do |io|
    while !io.eof?
      lineparts = io.readline.split(" ", 2).map(&:strip)

      next if not lineparts.size == 2

      lineparts.each_slice(2) { |k,v| exifhash[k] = v }
   end
  end

  return exifhash
end

BLACKDIFF_MAX = 4

class Array
  def handle_data_dups
    iso = self.first
    return [iso, self.last.first] if self.last.size == 1

    whitelevels = self.last.map { |black, white| white }.uniq

    if whitelevels.size != 1
      $stderr.puts "ISO #{iso} has multiple variants with different white levels: #{self.last}"
      return [iso, [-1, -1]]
    end

    whitelevel = whitelevels[0]

    blacklevels = self.last.map { |black, white| black }.uniq

    if (blacklevels.max - blacklevels.min) > BLACKDIFF_MAX
      $stderr.puts "ISO #{iso} has multiple variants with too different black levels: #{self.last}"
      return [iso, [-1, -1]]
    end

    blacklevel = blacklevels.max

    return [iso, [blacklevel, whitelevel]]
  end
end

def print_sensor(black, white, iso = false)
  if iso
    isolist = " iso_list=\"#{iso.join(" ")}\""
  else
    isolist = ""
  end

  return "\t\t<Sensor black=\"#{black}\" white=\"#{white}\"#{isolist}/>"
end

make = model = uniquecameramodel = nil
sensors = {}
ARGV.each do |filename|
  exifhash = get_exif(filename)

  if (make and make != exifhash["Exif.Image.Make"]) or
      # (model and model != exifhash["Exif.Image.Model"]) or
      (uniquecameramodel and uniquecameramodel != exifhash["Exif.Image.UniqueCameraModel"])
    $stderr.puts "WARNING: #{filename} - " \
                 "all files must be from the same camera maker and model!"
    next
  end

  make = exifhash["Exif.Image.Make"]
  model = exifhash["Exif.Image.Model"]
  uniquecameramodel = exifhash["Exif.Image.UniqueCameraModel"]

  iso = get_iso(exifhash).to_i
  white = exifhash["Exif.SubImage1.WhiteLevel"].to_i

  blacks = exifhash["Exif.SubImage1.BlackLevel"].split().map(&:strip)

  blacks = blacks.map(&:to_r)

  # want average black level, but rounded to next int
  black = blacks.reduce(:+).to_f / blacks.size
  black = black.ceil.to_i

  dsc = [black, white]

  sensors[iso] = [] if not sensors[iso]
  sensors[iso] << dsc
end

# want sorted by increasing ISO level
sensors = sensors.sort

# and only unique values
sensors.map! { |iso, dscs| [iso, dscs.uniq.sort] }

# sometimes there may be duplicate data still.
# if white levels match, and black levels are not too different, just take the biggest black level
sensors.map!(&:handle_data_dups)

invsensors = {}

if make == "Canon"
  isohash = {}
  sensors.each do |iso, dsc|
    isohash[dsc] = [] if not isohash[dsc]
    isohash[dsc] << iso
  end

  tmp = {}
  isohash.each do |dsc, isos|
    isohash.each do |dsc2, isos2|
      # don't process diagonal
      next if dsc == dsc2

      # only if white level match
      next if dsc[1] != dsc2[1]

      b = [dsc[0], dsc2[0]]

      next if b.max - b.min > BLACKDIFF_MAX

      # ok, can take this blacklevel
      dsc[0] = b.max

      isos += isos2
    end

    tmp[dsc] = [] if not tmp[dsc]
    tmp[dsc] = (tmp[dsc] + isos).uniq.sort
  end

  invsensors = tmp
else
  invsensors = {}
  sensors.each do |iso, dsc|
    invsensors[dsc] = [] if not invsensors[dsc]
    invsensors[dsc] << iso
  end
end

if true
  puts "invsensors #{invsensors}"
  puts
end

# so, which [black, white] is the most common ?
mostfrequent = invsensors.sort{ |x,y| y[1].flatten.size <=> x[1].flatten.size }.first
invsensors.delete(mostfrequent.first)

if true
  puts "sensors #{sensors}"
  puts "mostfrequent #{mostfrequent}"
  puts "invsensors #{invsensors}"
  puts
  # exit
end

mostfrequent = mostfrequent.first

puts "\t<Camera make=\"#{make}\" model=\"#{model}\">"
puts "\t\t<ID make=\"\" model=\"\">#{uniquecameramodel}</ID>"

puts print_sensor(mostfrequent.first, mostfrequent.last)
invsensors.each do |dsc, isos|
  puts print_sensor(dsc.first, dsc.last, isos)
end

puts "\t</Camera>"

# vim: tabstop=2 expandtab shiftwidth=2 softtabstop=2
# kate: tab-width: 2; replace-tabs on; indent-width 2; tab-indents: off;
# kate: indent-mode ruby; remove-trailing-spaces modified;
back to top