#!/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