timestamp_maker 1.2.1 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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