rpa 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 910fe19134498703c39f1af1504173fd102d4917
4
+ data.tar.gz: 7423f9dbf65947a84d3cc1fda3d41adac7f1ed2b
5
+ SHA512:
6
+ metadata.gz: 4a07f1db5a02faadd80196a806f6878c630347edfc32c3795667454be6d304534816a559dfb36cbab807d4fc1bf06dfcab72fe3320f20f875203564621f4c1ce
7
+ data.tar.gz: a6f13add15df7731af4d1ee75044866baf7cdfeffa7c81b393bb6ff87682784025ce53f347bec1cb45079b1be31850925072ae663259f922b81f81a57a6f4853
@@ -0,0 +1,17 @@
1
+ *.gem
2
+ *.rbc
3
+ .bundle
4
+ .config
5
+ .yardoc
6
+ Gemfile.lock
7
+ InstalledFiles
8
+ _yardoc
9
+ coverage
10
+ doc/
11
+ lib/bundler/man
12
+ pkg
13
+ rdoc
14
+ spec/reports
15
+ test/tmp
16
+ test/version_tmp
17
+ tmp
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in rpa.gemspec
4
+ gemspec
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2014 Andrew Tongen
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,38 @@
1
+ # Rpa
2
+
3
+ Generate simple html photo albums.
4
+
5
+ ## Installation
6
+
7
+ $ gem install rpa
8
+
9
+ ## Usage
10
+
11
+ ```
12
+ Usage: $ rpa [options]
13
+
14
+ Default values:
15
+ verbose = false
16
+ title = Photo Album
17
+
18
+ Options:
19
+ -i, --in-dir [IN_DIR]
20
+ -o, --out-dir [OUT_DIR]
21
+ -v, --[no-]verbose
22
+ -t, --title [TITLE]
23
+ --help
24
+ ```
25
+
26
+ ## Why?
27
+
28
+ This was a single-file script that I wrote a while ago and then
29
+ forgot about. I needed to use it recently, so I thought I'd
30
+ gemify and publish it.
31
+
32
+ ## Contributing
33
+
34
+ 1. Fork it ( http://github.com/andrew/rpa/fork )
35
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
36
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
37
+ 4. Push to the branch (`git push origin my-new-feature`)
38
+ 5. Create new Pull Request
@@ -0,0 +1 @@
1
+ require "bundler/gem_tasks"
data/bin/rpa ADDED
@@ -0,0 +1,2 @@
1
+ require 'rpa'
2
+ Rpa::Runner.new(ARGV).run!
@@ -0,0 +1,25 @@
1
+ require "rpa/version"
2
+ require "pathname"
3
+
4
+ module Rpa
5
+ PHOTO_TYPES = %w{ jpg jpeg png gif tiff tif bmp }
6
+
7
+ def self.check_system
8
+ if %x{ which jhead }.to_s.strip == ""
9
+ STDERR.puts "This program requires the jhead binary to be found in PATH"
10
+ exit 1
11
+ end
12
+ end
13
+
14
+ def self.root
15
+ @root ||= Pathname.new(File.expand_path('../..', __FILE__))
16
+ end
17
+
18
+ end
19
+
20
+ require "rpa/util"
21
+ require "rpa/options"
22
+ require "rpa/asset"
23
+ require "rpa/photos"
24
+ require "rpa/page"
25
+ require "rpa/runner"
@@ -0,0 +1,28 @@
1
+ require 'erb'
2
+
3
+ module Rpa
4
+ class Asset
5
+
6
+ def initialize(*o)
7
+ @options = *o.pop if o.last.is_a?(Hash)
8
+ end
9
+
10
+ def html(dir, title, photo_map)
11
+ build('app.html.erb', binding)
12
+ end
13
+
14
+ def js
15
+ build('app.js.erb', binding)
16
+ end
17
+
18
+ def css
19
+ build('app.css.erb', binding)
20
+ end
21
+
22
+ private
23
+
24
+ def build(template, local_binding)
25
+ ERB.new(File.read(Rpa.root.join('templates', template))).result(local_binding)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,61 @@
1
+ require 'optparse'
2
+
3
+ module Rpa
4
+ class Options
5
+
6
+ attr_reader :args,
7
+ :options
8
+
9
+ def initialize(args)
10
+ @args = args.dup
11
+ # set the default options
12
+ @options = {
13
+ :verbose => false,
14
+ :title => "Photo Album"
15
+ }
16
+ end
17
+
18
+ def parse!
19
+ OptionParser.new do |opts|
20
+ opts.banner = "Usage: $ rpa [options]\n\n" +
21
+ "Default values:\n" + @options.map {|v| "\t#{v[0]} = #{v[1]}" }.join("\n") + "\n\nOptions:"
22
+
23
+ # required arguments
24
+
25
+ opts.on("-i", "--in-dir [IN_DIR]") do |arg|
26
+ @options[:in_dir] = arg
27
+ end
28
+
29
+ opts.on("-o", "--out-dir [OUT_DIR]") do |arg|
30
+ @options[:out_dir] = arg
31
+ end
32
+
33
+ # optional arguments
34
+
35
+ opts.on("-v", "--[no-]verbose") do |arg|
36
+ @options[:verbose] = arg
37
+ end
38
+
39
+ opts.on("-t", "--title [TITLE]") do |arg|
40
+ @options[:title] = arg
41
+ end
42
+
43
+ opts.on_tail("--help") do
44
+ puts opts
45
+ exit 0
46
+ end
47
+ end.parse!(args)
48
+ validate!
49
+ self
50
+ end
51
+
52
+ def validate!
53
+ # validate options
54
+ if @options[:in_dir].to_s.strip == "" ||
55
+ !File.directory?(@options[:in_dir])
56
+ raise "IN_DIR is required."
57
+ end
58
+ end
59
+
60
+ end
61
+ end
@@ -0,0 +1,26 @@
1
+ module Rpa
2
+ class Page
3
+
4
+ def initialize(dir, title, photo_map, verbose = false)
5
+ @dir = dir
6
+ @title = title
7
+ @photo_map = photo_map
8
+ @verbose = verbose
9
+ asset = Asset.new
10
+ puts "Writing index.html" if verbose?
11
+ File.open(File.join(@dir, "index.html"), "w") { |f| f << asset.html(@dir, @title, @photo_map) }
12
+
13
+ puts "Writing app.css" if verbose?
14
+ File.open(File.join(@dir, "app.css"), "w") { |f| f << asset.css }
15
+
16
+ puts "Writing app.js" if verbose?
17
+ File.open(File.join(@dir, "app.js"), "w") { |f| f << asset.js }
18
+ end
19
+
20
+ private
21
+
22
+ def verbose?
23
+ !!@verbose
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,134 @@
1
+ # OUT_DIR structure
2
+ # /index.html
3
+ # /app.js
4
+ # /app.css
5
+ # /images/original/
6
+ # /images/preview/
7
+ # /images/thumb/
8
+
9
+ require 'RMagick'
10
+ require 'fileutils'
11
+
12
+ class Photos
13
+ include Magick
14
+
15
+ PREVIEW_SIZE = 500
16
+ THUMB_SIZE = 75
17
+
18
+ attr_reader :photo_map
19
+
20
+ def initialize(dir, photos = [], verbose = false)
21
+ @dir = dir
22
+ @photo_map = {}
23
+ photos.each do |p|
24
+ file_name = File.basename(p)
25
+ while @photo_map.has_key?(file_name)
26
+ ext = File.extname(file_name)
27
+ basename = File.basename(file_name, ext)
28
+ if m = basename.match(/\-([\d]+)$/)
29
+ file_name = "#{basename}-#{m[1].to_i+1}#{ext}"
30
+ else
31
+ file_name = "#{basename}-0#{ext}"
32
+ end
33
+ end
34
+ @photo_map[p] = file_name
35
+ end
36
+ @verbose = verbose
37
+
38
+ setup_dirs
39
+ write
40
+ fix_orientation
41
+ end
42
+
43
+ def original_dir
44
+ @original_dir ||= File.join(@dir, "images", "original")
45
+ end
46
+
47
+ def preview_dir
48
+ @preview_dir ||= File.join(@dir, "images", "preview")
49
+ end
50
+
51
+ def thumb_dir
52
+ @thumb_dir ||= File.join(@dir, "images", "thumb")
53
+ end
54
+
55
+ private
56
+
57
+ def verbose?
58
+ !!@verbose
59
+ end
60
+
61
+ def load_image(path)
62
+ ri = ImageList.new(path)
63
+ if path.match(/(jpg|jpeg)$/i)
64
+ # fix orientation from exif data
65
+ case ri.orientation.to_i
66
+ when 2
67
+ ri.flop!
68
+ when 3
69
+ ri.rotate!(180)
70
+ when 4
71
+ ri.flip!
72
+ when 5
73
+ ri.transpose!
74
+ when 6
75
+ ri.rotate!(90)
76
+ when 7
77
+ ri.transverse!
78
+ when 8
79
+ ri.rotate!(270)
80
+ end
81
+ end
82
+ ri
83
+ end
84
+
85
+ def setup_dirs
86
+ if File.directory?(original_dir)
87
+ puts "Removing existing original image directory" if verbose?
88
+ FileUtils.rm_rf(original_dir)
89
+ end
90
+ FileUtils.mkdir_p(original_dir)
91
+
92
+ if File.directory?(preview_dir)
93
+ puts "Removing existing preview image directory" if verbose?
94
+ FileUtils.rm_rf(preview_dir)
95
+ end
96
+ FileUtils.mkdir_p(preview_dir)
97
+
98
+ if File.directory?(thumb_dir)
99
+ puts "Removing existing thumb image directory" if verbose?
100
+ FileUtils.rm_rf(thumb_dir)
101
+ end
102
+ FileUtils.mkdir_p(thumb_dir)
103
+ end
104
+
105
+ def write
106
+ @photo_map.each do |path,name|
107
+ puts "Writing original #{name}" if verbose?
108
+ ri = load_image(path)
109
+ ri.write(File.join(original_dir, name))
110
+
111
+ puts "Writing preview #{name}" if verbose?
112
+ ri.resize_to_fit!(PREVIEW_SIZE)
113
+ ri.write(File.join(preview_dir, name))
114
+
115
+ puts "Writing thumb #{name}" if verbose?
116
+ ri.resize_to_fill!(THUMB_SIZE)
117
+ ri.write(File.join(thumb_dir, name))
118
+
119
+ ri.destroy!
120
+ end
121
+ end
122
+
123
+ def fix_orientation
124
+ jpegs = @photo_map.values.select { |p| p.match(/(jpg|jpeg)$/i) }.map do |r|
125
+ [original_dir, preview_dir, thumb_dir].map do |d|
126
+ "\"#{File.join(d, r)}\""
127
+ end
128
+ end.flatten.compact.join(' ')
129
+ unless jpegs == ""
130
+ puts "Removing EXIF orientation" if verbose?
131
+ %x{ jhead -norot #{jpegs} }
132
+ end
133
+ end
134
+ end
@@ -0,0 +1,38 @@
1
+ require 'fileutils'
2
+
3
+ module Rpa
4
+ class Runner
5
+
6
+ attr_reader :in_dir,
7
+ :out_dir,
8
+ :photos
9
+
10
+ def initialize(args)
11
+ @options = Options.new(args).parse!.options
12
+
13
+ @in_dir = File.expand_path(@options[:in_dir])
14
+ @photos = Dir["#{in_dir}/**/*"].select do |f|
15
+ %x{ file -ib "#{f}" }.to_s.strip.match(/^image/) && PHOTO_TYPES.any? { |t| f.match(/#{t}$/i) }
16
+ end.sort do |x,y|
17
+ Util.get_file_creation_time(x) <=> Util.get_file_creation_time(y)
18
+ end
19
+ if photos.length == 0
20
+ raise "IN_DIR must contain at least one photo."
21
+ end
22
+
23
+ @out_dir = File.expand_path(@options[:out_dir])
24
+ if File.exists?(out_dir)
25
+ unless File.directory?(out_dir)
26
+ raise "OUT_DIR exists and is not a directory."
27
+ end
28
+ end
29
+ end
30
+
31
+ def run!
32
+ FileUtils.mkdir_p(out_dir)
33
+ p = Photos.new(out_dir, photos, @options[:verbose])
34
+ Page.new(out_dir, @options[:title], p.photo_map, @options[:verbose])
35
+ end
36
+
37
+ end
38
+ end
@@ -0,0 +1,24 @@
1
+ require 'exifr'
2
+
3
+ module Rpa
4
+ module Util
5
+ extend self
6
+
7
+ def get_file_creation_time(path)
8
+ ext = File.extname(path)
9
+ t = if m = ext.downcase.match(/(tiff|tif|jpg|jpeg)$/)
10
+ exif_class = m[1][0] == 't' ? EXIFR::TIFF : EXIFR::JPEG
11
+ begin
12
+ exif = exif_class.new(path)
13
+ exif.date_time ||
14
+ exif.date_time_original ||
15
+ exif.date_time_digitized
16
+ rescue => e
17
+ STDERR.puts "Error getting EXIF data for #{path}: #{e.message}"
18
+ end
19
+ end
20
+ t ||= File::Stat.new(path).ctime
21
+ end
22
+
23
+ end
24
+ end