timestamp_maker 1.2.1 → 1.3.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: '045783f7b5b5a84f233d29cabecbd024978ba089878c094e4c8f3f8750f884dc'
4
- data.tar.gz: b2225e7ff4eca4da0e87593e4ad7ab704c87d4e5a75c46c86fc191ed083b5e69
3
+ metadata.gz: e7f96f255213ceac42648b8ad1317b745d64062f6ad116fd8365d64408077101
4
+ data.tar.gz: 6c4c191ee701c7ffbf539b9665df9b3064608f50cf7eee06cffede24f46aa7a4
5
5
  SHA512:
6
- metadata.gz: 9af1f4d12c9f92f6b08678966c4ff4f722706f2b9f633aaa739154399380090c28cff6d6fa3d8c126d5989e9b986c0e94c67c4aeae52d92b73976a6afe380f37
7
- data.tar.gz: b015042b64a96b483db3c2f16e3f5f236b33150cbddced62c845703ce644ad76b88dba3d87167ed264192b5e6d520211e37fcb1f379ca1ee088e9db89af58c8d
6
+ metadata.gz: 0f4e5a1e7f61304642cc758d82a4ec2a95af43a4f10fda93cb1f813468a1f6a6a321da5606acca79ad8724ca9ed79af162b8bedf82b3d9c0cae51ffd59a111de
7
+ data.tar.gz: 02712023f7dcd89f9083b95074ddde9770c6b73083e314d380bc71dd5020896a650ef509f79ff7183821f5ca18193805c2c1e9cc5a8cd65624ff5796bbe74214
data/bin/timestamp CHANGED
@@ -16,7 +16,8 @@ options = {
16
16
  coordinate_origin: 'top-left',
17
17
  x: 32,
18
18
  y: 32,
19
- font_padding: 8
19
+ font_padding: 8,
20
+ require: []
20
21
  }
21
22
 
22
23
  option_parser = OptionParser.new do |parser|
@@ -69,6 +70,10 @@ option_parser = OptionParser.new do |parser|
69
70
  parser.on('--font-padding NUM', Integer, 'Defaults to 8.') do |value|
70
71
  options[:font_padding] = value
71
72
  end
73
+
74
+ parser.on('-r LIB', '--require LIB', Array, 'Comma-separated Ruby libs') do |value|
75
+ options[:require] = value
76
+ end
72
77
  end
73
78
  option_parser.parse!
74
79
 
@@ -80,7 +85,8 @@ end
80
85
  input = ARGV.shift
81
86
  output = ARGV.shift
82
87
 
83
- TimestampMaker.add_timestamp(
88
+ options[:require].each { |i| require i }
89
+ TimestampMaker.instance.add_timestamp(
84
90
  input,
85
91
  output,
86
92
  format: options[:format],
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require 'marcel'
4
3
  require 'pathname'
5
- require 'timestamp_maker/video_timestamper'
6
- require 'timestamp_maker/mime_recognizer'
7
- require 'timestamp_maker/image_timestamper'
4
+ require 'timestamp_maker/handlers/image_magick'
5
+ require 'timestamp_maker/handlers/ffmpeg'
6
+ require 'timestamp_maker/mime_recognizers/marcel'
7
+ require 'timestamp_maker/time_zone_lookupers/wheretz'
8
8
  require 'tzinfo'
9
9
 
10
- module TimestampMaker
10
+ class TimestampMaker
11
11
  COORDINATE_ORIGINS = %w[
12
12
  top-left
13
13
  top-right
@@ -15,55 +15,69 @@ module TimestampMaker
15
15
  bottom-right
16
16
  ].freeze
17
17
 
18
- @mime_recognizer = MimeRecognizer
19
- @image_timestamper = ImageTimestamper
20
- @video_timestamper = VideoTimestamper
18
+ attr_accessor :mime_recognizer, :handlers
21
19
 
22
- class << self
23
- attr_reader :mime_recognizer, :image_timestamper, :video_timestamper
20
+ singleton_class.attr_writer :instance
21
+ def self.instance
22
+ @instance ||= new
23
+ end
24
24
 
25
- def add_timestamp(
26
- input_path, output_path,
27
- format: '%Y-%m-%d %H:%M:%S',
28
- time: nil,
29
- font_size: 32,
30
- font_family: 'Sans',
31
- font_color: 'white',
32
- background_color: '#000000B3',
33
- time_zone: nil,
34
- coordinate_origin: 'top-left',
35
- x: 32,
36
- y: 32,
37
- font_padding: 8
38
- )
39
- mime_type = mime_recognizer.recognize(input_path)
40
- processor =
41
- case mime_type.split('/').first
42
- when 'image' then image_timestamper
43
- when 'video' then video_timestamper
44
- else raise "Unsupported MIME type: ##{mime_type}"
45
- end
46
- time = processor.creation_time(input_path) if time.nil?
47
- raise ArgumentError unless time.is_a?(Time)
25
+ def initialize(
26
+ mime_recognizer: MimeRecognizers::Marcel.new,
27
+ handlers: [
28
+ Handlers::ImageMagick.new(
29
+ time_zone_lookuper: TimeZoneLookupers::Wheretz.new
30
+ ),
31
+ Handlers::Ffmpeg.new(
32
+ time_zone_lookuper: TimeZoneLookupers::Wheretz.new
33
+ )
34
+ ]
35
+ )
36
+ @mime_recognizer = mime_recognizer
37
+ @handlers = handlers
38
+ end
48
39
 
49
- time.localtime(TZInfo::Timezone.get(time_zone)) unless time_zone.nil?
40
+ def add_timestamp(
41
+ input_path, output_path,
42
+ format: '%Y-%m-%d %H:%M:%S',
43
+ time: nil,
44
+ font_size: 32,
45
+ font_family: 'Sans',
46
+ font_color: 'white',
47
+ background_color: '#000000B3',
48
+ time_zone: nil,
49
+ coordinate_origin: 'top-left',
50
+ x: 32,
51
+ y: 32,
52
+ font_padding: 8
53
+ )
54
+ mime_type = mime_recognizer.recognize(input_path)
55
+ handler = handlers.find { |i| i.accept?(mime_type) }
56
+ raise "Unsupported MIME type: ##{mime_type}" if handler.nil?
50
57
 
51
- unless COORDINATE_ORIGINS.include?(coordinate_origin)
52
- raise ArgumentError, "coordinate origin should be one of #{COORDINATE_ORIGINS.join(',')}"
53
- end
58
+ time = handler.creation_time(input_path) if time.nil?
59
+ raise ArgumentError unless time.is_a?(Time)
54
60
 
55
- processor.add_timestamp(
56
- input_path, output_path, time,
57
- format: format,
58
- font_size: font_size,
59
- font_family: font_family,
60
- font_color: font_color,
61
- background_color: background_color,
62
- coordinate_origin: coordinate_origin,
63
- x: x,
64
- y: y,
65
- font_padding: font_padding
61
+ time.localtime(TZInfo::Timezone.get(time_zone)) unless time_zone.nil?
62
+
63
+ unless COORDINATE_ORIGINS.include?(coordinate_origin)
64
+ raise(
65
+ ArgumentError,
66
+ "coordinate origin should be one of #{COORDINATE_ORIGINS.join(',')}"
66
67
  )
67
68
  end
69
+
70
+ handler.add_timestamp(
71
+ input_path, output_path, time,
72
+ format: format,
73
+ font_size: font_size,
74
+ font_family: font_family,
75
+ font_color: font_color,
76
+ background_color: background_color,
77
+ coordinate_origin: coordinate_origin,
78
+ x: x,
79
+ y: y,
80
+ font_padding: font_padding
81
+ )
68
82
  end
69
83
  end
@@ -6,9 +6,20 @@ require 'open3'
6
6
  require 'English'
7
7
  require 'tzinfo'
8
8
 
9
- module TimestampMaker
10
- module VideoTimestamper
11
- class << self
9
+ class TimestampMaker
10
+ module Handlers
11
+ class Ffmpeg
12
+
13
+ attr_accessor :time_zone_lookuper
14
+
15
+ def initialize(time_zone_lookuper:)
16
+ @time_zone_lookuper = time_zone_lookuper
17
+ end
18
+
19
+ def accept?(mime_type)
20
+ mime_type.start_with?('video/')
21
+ end
22
+
12
23
  def add_timestamp(
13
24
  input_path,
14
25
  output_path,
@@ -45,13 +56,14 @@ module TimestampMaker
45
56
  ]
46
57
 
47
58
  tz = tz_env_string(time)
48
- raise "Command failed with exit #{$CHILD_STATUS.exitstatus}: #{command.first}" unless system({ 'TZ' => tz }, *command)
59
+ raise "Command failed with exit #{$CHILD_STATUS.exitstatus}: #{command.first}" unless system({ 'TZ' => tz },
60
+ *command)
49
61
  end
50
62
 
51
63
  def creation_time(input_path)
52
64
  command = %W[
53
65
  ffprobe -v warning -print_format json
54
- -show_entries format_tags=creation_time,com.apple.quicktime.creationdate
66
+ -show_entries format_tags=creation_time,com.apple.quicktime.creationdate,location
55
67
  #{input_path}
56
68
  ]
57
69
  stdout_string, status = Open3.capture2(*command)
@@ -61,11 +73,29 @@ module TimestampMaker
61
73
  iso8601_string = parsed['format']['tags']['com.apple.quicktime.creationdate'] || parsed['format']['tags']['creation_time']
62
74
  raise 'Cannot find creation time' if iso8601_string.nil?
63
75
 
64
- Time.iso8601(iso8601_string)
76
+ time = Time.iso8601(iso8601_string)
77
+
78
+ iso6709_string = parsed['format']['tags']['location']
79
+ if iso6709_string && (time_zone = retrieve_time_zone_from_iso6709(iso6709_string))
80
+ begin
81
+ return TZInfo::Timezone.get(time_zone).to_local(time)
82
+ rescue TZInfo::InvalidTimezoneIdentifier
83
+ warn "Can not find time zone: #{time_zone}"
84
+ end
85
+ end
86
+
87
+ time
65
88
  end
66
89
 
67
90
  private
68
91
 
92
+ def retrieve_time_zone_from_iso6709(string)
93
+ data = string.match(/([+-]\d*\.?\d*)([+-]\d*\.?\d*)/)
94
+ latitude = data[1].to_f
95
+ longitude = data[2].to_f
96
+ time_zone_lookuper.lookup(latitude: latitude, longitude: longitude)
97
+ end
98
+
69
99
  def tz_env_string(time)
70
100
  return time.zone.name if time.zone.is_a? TZInfo::Timezone
71
101
 
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'open3'
4
+ require 'time'
5
+ require 'English'
6
+ require 'tzinfo'
7
+
8
+ class TimestampMaker
9
+ module Handlers
10
+ class ImageMagick
11
+ GRAVITY_MAP = {
12
+ 'top-left' => 'NorthWest',
13
+ 'top-right' => 'NorthEast',
14
+ 'bottom-left' => 'SouthWest',
15
+ 'bottom-right' => 'SouthEast'
16
+ }.freeze
17
+
18
+ attr_accessor :time_zone_lookuper
19
+
20
+ def initialize(time_zone_lookuper:)
21
+ @time_zone_lookuper = time_zone_lookuper
22
+ end
23
+
24
+ def accept?(mime_type)
25
+ mime_type.start_with?('image/')
26
+ end
27
+
28
+ def add_timestamp(
29
+ input_path,
30
+ output_path,
31
+ time,
32
+ format:,
33
+ font_size:,
34
+ font_family:,
35
+ font_color:,
36
+ background_color:,
37
+ coordinate_origin:,
38
+ x:,
39
+ y:,
40
+ font_padding:
41
+ )
42
+ time_string = time.strftime(format)
43
+ command = %W[
44
+ convert #{input_path}
45
+ (
46
+ -background #{background_color}
47
+ -fill #{font_color}
48
+ -family #{font_family}
49
+ -pointsize #{font_size}
50
+ -gravity NorthWest
51
+ -splice #{font_padding}x#{font_padding}
52
+ -gravity SouthEast
53
+ -splice #{font_padding}x#{font_padding}
54
+ label:#{time_string}
55
+ )
56
+ -gravity #{GRAVITY_MAP[coordinate_origin]}
57
+ -geometry +#{x}+#{y}
58
+ -composite #{output_path}
59
+ ]
60
+ raise "Command failed with exit #{$CHILD_STATUS.exitstatus}: #{command.first}" unless system(*command)
61
+ end
62
+
63
+ def creation_time(input_path)
64
+ command = %W[
65
+ identify -format %[exif:DateTime*]%[exif:OffsetTime*]%[exif:GPSLatitude*]%[exif:GPSLongitude*] #{input_path}
66
+ ]
67
+
68
+ stdout_string, status = Open3.capture2(*command)
69
+ raise unless status.success?
70
+
71
+ parsed = Hash[stdout_string.split("\n").map! { |i| i[5..-1].split('=') }]
72
+
73
+ time_string = parsed['DateTimeOriginal'] || parsed['DateTimeDigitized'] || parsed['DateTime']
74
+ raise 'Cannot find creation time' if time_string.nil?
75
+
76
+ time_arguments = time_string.split(/[: ]/).map(&:to_i)
77
+
78
+ if (time_zone = retrieve_time_zone_by_coordinate(parsed))
79
+ begin
80
+ return TZInfo::Timezone.get(time_zone).local_time(*time_arguments)
81
+ rescue TZInfo::InvalidTimezoneIdentifier
82
+ warn "Can not find time zone: #{time_zone}"
83
+ end
84
+ end
85
+
86
+ time_offset_string = parsed['OffsetTimeOriginal'] || parsed['OffsetTimeDigitized'] || parsed['OffsetTime']
87
+ raise 'Can not find time offset' if time_offset_string.nil?
88
+
89
+ Time.new(*time_arguments, time_offset_string)
90
+ end
91
+
92
+ private
93
+
94
+ def parse_coordinate_number(string)
95
+ degree, minute, second = string.split(', ').map! { |i| Rational(i) }
96
+ (degree + minute / 60 + second / 3600).to_f
97
+ end
98
+
99
+ def retrieve_time_zone_by_coordinate(exif)
100
+ unless exif['GPSLatitude'] && exif['GPSLatitudeRef'] && exif['GPSLongitude'] && exif['GPSLongitudeRef']
101
+ return nil
102
+ end
103
+
104
+ latitude = parse_coordinate_number(exif['GPSLatitude'])
105
+ latitude = -latitude if exif['GPSLatitudeRef'] == 'S'
106
+ longitude = parse_coordinate_number(exif['GPSLongitude'])
107
+ longitude = -longitude if exif['GPSLongitudeRef'] == 'W'
108
+
109
+ time_zone_lookuper.lookup(latitude: latitude, longitude: longitude)
110
+ end
111
+ end
112
+ end
113
+ end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'marcel'
4
+
5
+ class TimestampMaker
6
+ module MimeRecognizers
7
+ class Marcel
8
+ def recognize(path)
9
+ ::Marcel::MimeType.for(Pathname.new(path))
10
+ end
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'net/http'
5
+ require 'uri'
6
+
7
+ class TimestampMaker
8
+ module TimeZoneLookupers
9
+ ENDPOINT = URI.parse('http://api.geonames.org/timezoneJSON')
10
+
11
+ class GeoName
12
+ attr_accessor :username
13
+
14
+ def initialize(username:)
15
+ @username = username
16
+ end
17
+
18
+ def lookup(latitude:, longitude:)
19
+ query = URI.encode_www_form(
20
+ [['lat', latitude], ['lng', longitude], ['username', username]]
21
+ )
22
+ response = Net::HTTP.get_response(URI.parse("#{ENDPOINT}?#{query}"))
23
+ raise "Got HTTP status code: #{response.code}" unless response.is_a?(Net::HTTPSuccess)
24
+
25
+ parsed = JSON.parse(response.body)
26
+ parsed['timezoneId']
27
+ end
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TimestampMaker
4
+ module TimeZoneLookupers
5
+ class Mock
6
+ def initialize(time_zone)
7
+ @time_zone = time_zone
8
+ end
9
+
10
+ def lookup(*)
11
+ @time_zone
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'wheretz'
4
+ require 'json' # wheretz forgets to require json
5
+
6
+ class TimestampMaker
7
+ module TimeZoneLookupers
8
+ class Wheretz
9
+ def lookup(latitude:, longitude:)
10
+ case result = WhereTZ.lookup(latitude, longitude)
11
+ when String then result
12
+ when Array then result.first
13
+ else raise 'Something went wrong with WhereTZ'
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: timestamp_maker
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.2.1
4
+ version: 1.3.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Weihang Jian
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-21 00:00:00.000000000 Z
11
+ date: 2021-07-25 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: marcel
@@ -38,6 +38,20 @@ dependencies:
38
38
  - - "~>"
39
39
  - !ruby/object:Gem::Version
40
40
  version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: wheretz
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - '='
46
+ - !ruby/object:Gem::Version
47
+ version: 0.0.6
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - '='
53
+ - !ruby/object:Gem::Version
54
+ version: 0.0.6
41
55
  - !ruby/object:Gem::Dependency
42
56
  name: minitest
43
57
  requirement: !ruby/object:Gem::Requirement
@@ -76,9 +90,12 @@ extra_rdoc_files: []
76
90
  files:
77
91
  - bin/timestamp
78
92
  - lib/timestamp_maker.rb
79
- - lib/timestamp_maker/image_timestamper.rb
80
- - lib/timestamp_maker/mime_recognizer.rb
81
- - lib/timestamp_maker/video_timestamper.rb
93
+ - lib/timestamp_maker/handlers/ffmpeg.rb
94
+ - lib/timestamp_maker/handlers/image_magick.rb
95
+ - lib/timestamp_maker/mime_recognizers/marcel.rb
96
+ - lib/timestamp_maker/time_zone_lookupers/geo_name.rb
97
+ - lib/timestamp_maker/time_zone_lookupers/mock.rb
98
+ - lib/timestamp_maker/time_zone_lookupers/wheretz.rb
82
99
  homepage: https://github.com/tonytonyjan/timestamp_maker
83
100
  licenses:
84
101
  - MIT
@@ -1,69 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'open3'
4
- require 'time'
5
- require 'English'
6
-
7
- module TimestampMaker
8
- module ImageTimestamper
9
- GRAVITY_MAP = {
10
- 'top-left' => 'NorthWest',
11
- 'top-right' => 'NorthEast',
12
- 'bottom-left' => 'SouthWest',
13
- 'bottom-right' => 'SouthEast'
14
- }.freeze
15
-
16
- def self.add_timestamp(
17
- input_path,
18
- output_path,
19
- time,
20
- format:,
21
- font_size:,
22
- font_family:,
23
- font_color:,
24
- background_color:,
25
- coordinate_origin:,
26
- x:,
27
- y:,
28
- font_padding:
29
- )
30
- time_string = time.strftime(format)
31
- command = %W[
32
- convert #{input_path}
33
- (
34
- -background #{background_color}
35
- -fill #{font_color}
36
- -family #{font_family}
37
- -pointsize #{font_size}
38
- -gravity NorthWest
39
- -splice #{font_padding}x#{font_padding}
40
- -gravity SouthEast
41
- -splice #{font_padding}x#{font_padding}
42
- label:#{time_string}
43
- )
44
- -gravity #{GRAVITY_MAP[coordinate_origin]}
45
- -geometry +#{x}+#{y}
46
- -composite #{output_path}
47
- ]
48
- raise "Command failed with exit #{$CHILD_STATUS.exitstatus}: #{command.first}" unless system(*command)
49
- end
50
-
51
- def self.creation_time(input_path)
52
- command = %W[
53
- identify -format %[exif:DateTime*]%[exif:OffsetTime*] #{input_path}
54
- ]
55
-
56
- stdout_string, status = Open3.capture2(*command)
57
- raise unless status.success?
58
-
59
- parsed = Hash[stdout_string.split("\n").map! { |i| i[5..-1].split('=') }]
60
-
61
- time_string = parsed['DateTimeOriginal'] || parsed['DateTimeDigitized'] || parsed['DateTime']
62
- raise 'Cannot find creation time' if time_string.nil?
63
-
64
- time_offset_string = parsed['OffsetTimeOriginal'] || parsed['OffsetTimeDigitized'] || parsed['OffsetTime'] || 'Z'
65
-
66
- Time.strptime("#{time_string} #{time_offset_string}", '%Y:%m:%d %H:%M:%S %Z')
67
- end
68
- end
69
- end
@@ -1,9 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module TimestampMaker
4
- module MimeRecognizer
5
- def self.recognize(path)
6
- Marcel::MimeType.for(Pathname.new(path))
7
- end
8
- end
9
- end