Newer
Older
webtls / rjpg
@HIROSE Yuuji HIROSE Yuuji on 2 Jun 2018 16 KB git gateway started
#!/usr/bin/env ruby
# coding: euc-jp
# Resize jpeg files
# REQUIRE: Ruby, jhead, NetPBM
# $Id: rjpg,v 1.23 2014/08/26 06:10:00 yuuji Exp yuuji $
# Last modified Sun Jan 31 23:34:11 2016 on firestorm

# Typical Usege:
# (In photo files directory)
#	% mkdir 1024
#	% cd 1024
#	% rjpg -l 1024 ../*.jpg

# 基本的な使い方
# あるディレクトリにたくさんの大きなJPEGファイル(デジカメで撮ったままの
# ファイルとか)があるときに、それらをカレントディレクトリに適当なサイズ/
# 明るさに一括変換して吐き出す。吐き出したjpgファイルのコメントエリアに
# はそのファイルのタイムスタンプが埋め込まれる。
#
# 【WWW用に幅400くらいにしたい】
#	% rjpg -x 400 /dos/d/*jpg
# 【やっぱりもとの半分のサイズで】
#	% rjpg -s 0.5 -f /dos/d/*jpg		(-f付けないと上書きしない)
# 【縦横長い方の辺を800ピクセルに】
#	% rjpg -l 400 /dos/d/*jpg
# 【γ値1.5で】
#	% rjpg -x 400 -g 1.5 /dos/d/*.jpg	(デフォルトは1.0)
# 【90度回転して】
#	% rjpg -x 400 -r 90 /dos/d/dcp01234.jpg
# 【中央部70%だけ残るようトリミング】
#	% rjpg -x 600 -c 70% /dos/d/dcp01235.jpg

$rdjpg		= "rdjpgcom"
wrjpg		= "wrjpgcom"
djpg		= "djpeg"
cjpg		= "cjpeg"
pnmcut		= "pnmcut"
cat		= "cat"
outputdir	= "."
resize		= false
quality		= ENV["RJPG_q"] || "75"
gamma		= ENV["RJPG_g"] || "1.0"
tformat		= "%a %b %d %T %Y %Z"
uniqfile	= false
uniqonly	= false
gammafmt	= "| pnmgamma %s"
scalefmt	= "pnmscale -xsize %d -ysize %d"
`sh -c "type pamscale"`
if $?.to_i == 0 then
  scalefmt	= "pamscale -xsize %d -ysize %d"
  gammafmt	= "| pnmgamma -bt709tolin -gamma=%s -"
end
rotatefmt	= "| pnmrotate %f"
fnformat	= "i%Y%m%d-%H%M%S-"
fnsuffixfigure	= 2
xtable		= File.basename($0) + ".xtb"

$debug		= false
scale		= false
width		= false
force		= false
noforce		= false
rotate		= 0
fntitle		= false
Yfix		= false
tsname		= false
mkxtable	= false
putexif		= system("/bin/sh", "-c", "type jhead >/dev/null 2>&1")
cutx		= nil
cuty		= nil
landscape	= nil
portrait	= nil

usage = <<_EOU_
#{File.basename($0)} [options]  JpegFiles
    Options are...
	-x WIDTH	Set new jpg file's width to WIDTH
	-y HEIGHT	Set new jpg file's height to HEIGHT
	-l LENGTH	Set new jpg file's longer edge length to LENGTH
	-s SCALE	Set scale (for pnmscale) to SCALE
	-o DIR		Output directory
	-q N		Jpeg compression quality
	-f		Force Overwrite
	-n		Not to Overwrite
	-g N		Gamma correction
	-r ANGLE	Rotate ANGLE degrees in unti-clockwise
	-c X,Y,W,H	Cut W*H rectangle from coordinate (X,Y)
	-c N%		Cut the N% center of photo
	-p FILE		Print File's parent file name filled in by rjpg
	-pt FILE DIR	Check if FILE's parent file is in DIR
	-rm [DIR]	Show every file which has no parent in DIR(..).
	-rmok [DIR]	Remove every file which has no parent in DIR(..).
	-P		Create progressive JPG
	-i		Use ImageMagick (Aprx. 4times slow)
	-fn		Put file name in jpg comment area, instead of time
	-ex		Put Exif data in jpg comment area (requires jhead)
	-ex-		Do not copy exif
	-tf FMT		Time format same as strftime(3)
	-u		Uniq File Name according to time stamp
	-uf FMT		(with -u)Filename format in strftime (#{fnformat})
	-sf N		(with -u)File name counter's figure (default: 3)
	-xt		(with -u)Create filename translation table
	-uo		Uniquify File Name Only, no conversion
	-fe		GUI Front-end (Call via viewer eg.gqview)

Environment variable $RJPG_OPTS values are prepended to command line arguments.
_EOU_
#'

def parent_file(file)
  if test(?s, file) then
    if /Original file name: (\S*)/ =~ `#{$rdjpg} '#{file}'`
      return $1
    end
  end
  return false
end

def parent(file)
  if pf=parent_file(file) then
    puts pf
    exit 0
  end
  exit 1
end

def parent_test(file, dir)
  if pf=parent_file(file) then
    test(?s, File.expand_path(pf, dir)) and exit(0) or exit 1
  end
end

def rm_orphan(dir, ok=false)
  dir = ".." unless dir
  for f in Dir.glob("*.jpg")
    if (pf=parent_file(f)) && !test(?s, File.expand_path(pf, dir)) then
      ok and File.unlink(f)
      printf("rm %s%s\n", f, ok ? " done." : "")
    end
  end
  exit 0
end

def rjpg_l(args, doln, l, q, oopts)
  myname = File.basename($0)
  # sleep 1

  label = TkLabel.new(:bg=>:white).pack(:side=>:top, :fill=>:both).anchor('w')
  f = TkFrame.new.pack(:side=>:top, :fill=>'x')
  sb = TkScrollbar.new(f).pack(:side=>:right, :fill=>:y)
  text = TkText.new(f, :width=>80, :height=>10) {
    # font 'yozfont 16 normal'
    value = ""
    font '-jis-fixed-*-r-normal--16-*'
    yscrollbar sb
    bg 'lemonchiffon'
  }.pack(:side=>:right)
  btn_bg = nil
  btn = TkButton.new(:text=>' STOP ') {
    btn_bg = bg
    command proc {
      text.value = "**** 中止 ****"
      text.update
      sleep 1
      exit 0
    }
    bg 'yellow'
    set_focus
  }.pack(:side=>:top, :fill=>:x)
  t1 = Thread.new {
    afig = sprintf("%%0%dd", args.length.to_s.length)
    upfmt = "["+afig+"/"+afig+"]"
    i = 0
    args.each {|path|
      dir, file = File.dirname(path), File.basename(path)
      outdir = l
      rjpg_opt = ['-f', '-l', l, '-q', q.to_s, '-o', outdir]
      rjpg_opt += oopts.split(" ") if oopts != ""
      cmdline = [$0] + rjpg_opt + [file]
      btn.text = "STOP "+sprintf(upfmt, i+=1, args.length)
      label.text = sprintf("%s %s %s", myname, rjpg_opt.join(' '), file)
      next unless test(?d, dir) && test(?x, dir) && test(?w, dir)
      # call rjpg
      pipe = IO.pipe
      Dir.chdir(dir) {
        Dir.mkdir(l) unless test(?d, l)
        Thread.new {
          pid = fork {
            STDOUT.reopen(pipe[1])
            STDERR.reopen(pipe[1])
            exec *cmdline
          }
          label.text += " (pid:#{pid})"
          Process.wait
          if $?.to_i==0
            pipe[1].puts myname + " on #{path} Done"
          end
          ext = File.extname(file)
          if $?.to_i==0 && ext && doln
            rootname = File.basename(file, ext)
            lnfile = sprintf("%s-%sq%02d%s", rootname, l, q.to_i, ext)
            newfile = sprintf("%s/%s", l, file)
            File.unlink(lnfile) if test(?f, lnfile)
            File.link(newfile, lnfile)
            pipe[1].puts "Link to #{dir}/#{lnfile}"
          end
          pipe[1].close
        }
        while line=pipe[0].gets
          text.value += line
          text.see('end')
        end
      }
    }
  }
  while t1.alive?
    text.update
    Thread.pass
  end  

  text.value += "===== Done. =====\n"
  text.see('end')
  #  TkButton.new(:text=>" OK ", :command=>proc {exit}).pack(:side=>:top)
  btn.configure(:text=>" OK ", :command=>proc {exit}, :bg=>btn_bg)
  Tk.root.bind('q', proc {exit 0})
end

def frontend(args)
  require 'tk'
  lmin, lmax, ldef = 200, 6000, 2400
  len = (ENV['RJPGX'] || ldef).to_i
  qua = ENV['RJPGQ'] || "85"
  len = ldef if len<lmin || len>lmax
  qua = (qua.to_i%100).to_s
  vlen = TkVariable.new
  vqua = TkVariable.new
  link = TkVariable.new
  opts = TkVariable.new
  bg = 'pink'
  # TkOption.add('*font', 'ipagothic 16')
  TkOption.add('*font', '-jis-fixed-*-r-normal--16-*')
  msg = TkMessage.new(:width=>"400") {
    text("指定した幅(または高さ)の縮小画像を、" + \
         "長さと同じ名前のサブディレクトリに作り、" + \
         "必要なら同一ディレクトリにハードリンクを張ります。")
    bg "ivory"
    justify "left"
  }.pack(:side=>:top, :fill=>:both)
  frame = TkFrame.new {|f|
    row = -1
    TkLabel.new(f, :text=>'長辺サイズ(pixel)').grid(:row=>row+=1, :column=>0)
    TkSpinbox.new(f) {
      textvariable	vlen
      values		lmin.step(lmax, 100).collect
      set		len.to_s
      bg		bg
    }.grid(:row=>row, :column=>1)

    TkLabel.new(f, :text=>'JPEG quality').grid(:row=>row+=1, :column=>0)
    TkSpinbox.new(f) {
      textvariable	vqua
      to		100
      from		0
      set		qua
      bg		bg
    }.grid(:row=>row, :column=>1)

    TkLabel.new(f, :text=>'追加オプション').grid(:row=>row+=1, :column=>0)
    TkEntry.new(f) {
      textvariable	opts
      bg		bg
    }.grid(:row=>row, :column=>1, :sticky=>:w)

    TkLabel.new(f, :text=>'ハードリンク作成').grid(:row=>row+=1, :column=>0)
    TkCheckbutton.new(f, :text=>'') {
      text		"YES"
      variable		link
      deselect
      command proc {text (link.value=="1" ? "YES" : "NO")}
    }.grid(:row=>row, :column=>1, :sticky=>:w)
  }.pack(:side =>:top, :fill=>'x', :expand=>true)
  TkFrame.new {|f|
    TkButton.new(f) {
      text	"GO"
      command	proc {
        f.unpack; msg.unpack; self.focus
        rjpg_l(args, link=="1", vlen.value, vqua.value, opts.value)
      }
    }.pack(:side=>:left)
    TkButton.new(f) {
      text	"Cancel"
      command	proc {
        STDERR.puts "bye"
        exit 0
      }
    }.pack(:side=>:right)
  }.pack(:side =>:top, :fill=>'x', :expand=>true)
  Tk.mainloop
  exit 0                        # should not be reached
end

# Prepend $RJPG_OPTS to ARGV
ENV['RJPG_OPTS'] &&  ARGV.unshift(*ENV['RJPG_OPTS'].split(/\s+/))

while (/^-.+/ =~ ($_ = ARGV[0]) && ARGV.shift)
  $_= $_.dup
  break if ~/^--$/
  while ~/^-[A-z]/
    if ~/^-x$/
      width = ARGV.shift.to_i
    elsif ~/^-y$/
      height = ARGV.shift.to_i
      Yfix = true;
    elsif ~/^-l$/
      length = ARGV.shift
    elsif ~/^-s$/
      scale = ARGV.shift
    elsif ~/^-d$/
      $debug = true
    elsif ~/^-o$/
      outputdir = ARGV.shift
    elsif ~/^-q$/
      quality = ARGV.shift
    elsif ~/^-f$/
      force = true
    elsif ~/^-n$/
      noforce = true
    elsif ~/^-g/
      gamma = ARGV.shift
    elsif ~/^-r$/
      rotate = ARGV.shift
    elsif ~/^-p$/
      parent(ARGV.shift)
    elsif ~/^-pt$/
      parent_test(ARGV.shift, ARGV.shift)
    elsif ~/^-rm$/
      rm_orphan(ARGV.shift)
    elsif ~/^-rmok$/
      rm_orphan(ARGV.shift, true)
    elsif ~/^-P$/
      cjpg << " -progressive"
    elsif ~/^-fn$/
      fntitle = true; $_.sub!(/./, '')
    elsif ~/^-ex$/
      putexif = true; $_.sub!(/./, '')
    elsif ~/^-ex-$/
      putexif = false; $_.sub!(/../, '')
    elsif ~/^-i$/
      #djpg = "convert - PNM:-"
      scalefmt = "convert -geometry %dx%d"
      gammafmt = " -gamma %d"
      rotatefmt	= " -rotate %f"
    elsif ~/^-c$/
      $_ = ARGV.shift
      if ~/(\d+)%/
	$cut = $1
      else
	$cut=$_
	cutx, cuty = $cut.split(/,/)[0][2..3]
	$cut = "#{pnmcut} " + $_.split(/,/).join(" ")
      end
    elsif ~/^-u$/
      uniqfile = true
    elsif ~/^-uf$/
      uniqfile = fnformat = ARGV.shift; $_.sub!(/u/, '')
    elsif ~/^-sf$/
      fnsuffixfigure = ARGV.shift.to_i; $_.sub!(/s/, '')
    elsif ~/^-xt$/
      mkxtable = true; $_.sub!(/x/, '')
    elsif ~/^-uo$/
      uniqonly = uniqfile = true; $_.sub!(/./, '')
    elsif ~/^-fe$/
      frontend(ARGV)
    else
      puts "Invalid option #{$_}"
      print usage
      exit 0
    end
    $_.sub!(/^-.(.*)/, '-\1')
  end
end

def intr ()
  unlink(outfile) if test(?f, outfile)
  STDERR.puts "User break: #{outfile} deleted"
  exit 0;
end

class String
  if defined?("a".getbyte)
    nil
  else
    def getbyte(n)
      return self[n]
    end
  end
end
def charAt(handle, at)
  handle.seek(at, 0)
  return handle.read(1).getbyte(0)
  return handle.read(1).unpack("C")[0]
end

def jpgsize(file)
  # Return [x, y, comment] if success, else nil
  filesize=File.size(file)
  return nil if filesize < 2	# must have jpeg id (2bytes) at least
  open(file, "r"){|h|
    buf = h.read(2)
    comment=""
    # return nil if buf[0] != 0xff || buf[1] != 0xd8
    # return nil if buf != "\xff\xd8"   # NG at 1.9.1
    # return nil if buf.getbyte(0)!=0xff && buf.getbyte(1)!=0xd8 # 1.9 OK
    return nil if buf.unpack('C*') != "\xff\xd8".unpack('C*')
    # return nil if buf.bytes.to_a != "\xff\xd8".bytes.to_a	# 1.9 OK
    seekpoint = 2
    catch (:exit) {
      while seekpoint < filesize-4
	if charAt(h, seekpoint) != 0xff then
	  throw :exit, 0
	elsif [192, 198, 194, 195, 197, 198, 199,
	    201, 202, 203, 205, 206, 207].index(charAt(h, 1+seekpoint)) then
	  # jpeg geometry area found!
	  h.seek(5+seekpoint, 0)
	  buf = h.read(4)
	  x = buf[2..3].unpack("n")[0]
	  y = buf[0..1].unpack("n")[0]
	  throw :exit, [x, y, comment]
	elsif 0xFE == charAt(h, 1+seekpoint) then
	  # JPEG comment area
	  h.seek(2+seekpoint, 0)
	  len = h.read(2).unpack("n")[0]
	  comment << h.read(len-2)+" "
	else
	  # Other marker
	  # Do nothing
	end
	seekpoint += 2
	h.seek(seekpoint, 0)
	buf = h.read(2)
	seekpoint += buf.unpack("n")[0]
      end
    }
  }
end

for f in ARGV
  if ! test(?f, f)
    STDERR.puts "No such file: #{f}";
    next
  end
  timestamp = File.mtime(f)
  comment = `#{$rdjpg} '#{f}'`.chomp
  origbase = File.basename(f)
  if comment > ""
    stamp = comment
    puts "#{f} Comment: #{stamp}" if $debug
  elsif ! fntitle
    # stamp = &timeformat($timestamp);
    stamp = Time.at(timestamp).strftime(tformat)
    puts "#{f} Stamp: #{stamp}" if $debug
  else
    stamp = origbase
  end
  if !scale
    x, y = jpgsize(f)
    if length
      if x>y
	landscape, portrait, width, height = true, nil, length.to_i, nil
      else
	landscape, portrait, width, height = nil, true, nil, length.to_i
      end
    end
  end
  if width && !Yfix
    #x, y = `#{djpg} #{f}|pnmfile -`.scan(/(\d+) by (\d+)/)[0]
    if width > x
      #STDERR.puts "New width > original.  Skipping #{f}"
      #next
      STDERR.puts "New width > original.  Keeping original size #{x}:#{y}"
      resize = sprintf(scalefmt, x, y)
    else
      if cutx && cuty
	height = width.to_i * cuty.to_i / cutx.to_i
      else
	height = y.to_i * width.to_i / x.to_i
      end
      ##resize = "pnmscale -xsize #{width} -ysize #{height}"
      resize = sprintf(scalefmt, width, height)
    end
  elsif height
    ##x, y = `#{djpg} #{f}|pnmfile -`.scan(/(\d+) by (\d+)/)[0]
    if height > y
      #STDERR.puts "New height >= original.  Skipping #{f}"
      #next
      STDERR.puts "New height > original.  Keeping original size #{x}:#{y}"
      resize = sprintf(scalefmt, x, y)
    else
      if cutx && cuty
	width = height.to_i * cutx.to_i / cuty.to_i
      else
	width = x.to_i * height.to_i / y.to_i
      end
      #resize = "pnmscale -xsize #{width} -ysize #{height}"
      resize = sprintf(scalefmt, width, height)
    end
  elsif scale
    if ~/convert/
      resize = sprintf("convert -geometry %d%% - -", (scale.to_i*100).round)
    else
      resize = "pnmscale #{scale}"
    end
  end

  # Construct file name
  outfile = ''
  fnsuffixfmt = "%0#{fnsuffixfigure}d.jpg"
  if uniqfile
    fbase = "#{outputdir}/" + Time.at(timestamp).strftime(fnformat)
    sc = "Original file name: #{origbase}" # stamp concats
    0.upto(10**fnsuffixfigure-1) {|i|
      outfile = fbase + sprintf(fnsuffixfmt, i)
      break unless test(?f, outfile)
      if system("sh", "-c", "jhead '#{outfile}'|fgrep '#{origbase}' >/dev/null 2>&1")
        File.unlink(outfile) if force && !noforce
	break
      end
    }
    stamp += "\n"+sc		# Append Original Filename to stamp string
    if mkxtable
      open(xtable, "a"){|x|
	x.printf "s/%s/%s/\n", origbase, File.basename(outfile)
      }
    end
    if uniqonly
      File.link(f, outfile)
      print "ln #{f} #{outfile}\n" unless $quiet
      next
    end
  else
    outfile = "#{outputdir}/" + origbase
  end
  # sleep(1);
  if test(?f, outfile)
    s1, s2 = File.stat(f), File.stat(outfile)
    if s1.dev==s2.dev && s1.ino==s2.ino || (!force && noforce)
      puts "Skipping #{f}..."
      next
    end
    if ! force
      outfile.sub!(/\.jpg$/, "\.jpeg")
      puts "Set out file to #{outfile}"
    end
  end
  trap(:INT, 'intr')
  open(f) do |img|
    img.binmode
    if resize
      if /#{pnmcut}/o =~ $cut
	cmdline = "#{djpg} | #{$cut}"
      elsif $cut && $cut.to_i > 0
	c = $cut.to_i
	ox, oy = `#{$rdjpg} -v '#{f}'`.scan(/(\d+)w \* (\d+)h/)[0]
	ox, oy = ox.to_i, oy.to_i
	x1, y1, w1, h1 =
	  ox/2*(100-c)/100,
	  oy/2*(100-c)/100,
	  ox*c/100,
	  oy*c/100
	cmdline = "%s | %s %d %d %d %d" % [djpg, pnmcut, x1, y1, w1, h1]
	# cmdline = "djpeg | pnmcut 264 175.2 1232 817.6"
	#cmdline = "djpeg | pnmcut 200 100 800 600 "
	# cmdline = "djpeg"
      else
	cmdline = djpg.dup
      end
      cmdline << "| #{resize}"
      cmdline << sprintf(" -gamma %d", gamma) if gamma.to_f != 1.0
      cmdline << sprintf(rotatefmt, rotate.to_f) if rotate.to_f%360 != 0
      if /convert/ =~ scalefmt
        cmdline << sprintf(" -quality %d - JPEG:-", quality)
      else
        cmdline << "| #{cjpg} -q #{quality}"
      end
      cmdline << " | #{wrjpg} -c \"#{stamp}\" > #{outfile}"
      puts "#{cat} #{f} | #{cmdline}"
      STDOUT.flush              # for `rjpg -fe'
      open("| #{cmdline}", "w") do |out|
	out.binmode
	out.sync=true
	#out.print img.readlines
	begin
	  out.print img.read
	rescue
	  STDERR.puts "#{f}: Trailing superfluous data ignored."
	end
      end
      trap('PIPE', 'DEFAULT')
    else				  # not resizing
      puts("#{wrjpg} -c \"#{stamp}\" #{f} > #{outfile}") unless $quiet
      open("| #{wrjpg} -c \"#{stamp}\"  > '#{outfile}'", "w") do |out|
	out.binmode
	begin
	  out.print img.read
	rescue
	  STDERR.puts "#{f}: Trailing superfluous data ignored."
	end
      end
    end
  end
  trap('INT', "DEFAULT")
  if putexif
    puts "jhead -te '#{f}' #{outfile}" unless $quiet
    system "jhead -te '#{f}' '#{outfile}'"
    tf = "#{outputdir}/rjpg-#{$$}.tmp"
    File.unlink(tf) if test(?f, tf)
    File.rename(outfile, tf)
    system "#{wrjpg} -c \"#{stamp}\" '#{tf}' > '#{outfile}'"
    File.unlink(tf)
  end
  File.utime(timestamp, timestamp, outfile)
end