timestamp_maker 1.0.2 → 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: 01abd157048ff7bdbf8fee5cd139f8e238c48dc82850202aa4f9c7748d7bf8da
4
- data.tar.gz: 7c94cd8a48aab112d17a48f97112cb5594fd517415518202e0dc6074e1b707a0
3
+ metadata.gz: e7f96f255213ceac42648b8ad1317b745d64062f6ad116fd8365d64408077101
4
+ data.tar.gz: 6c4c191ee701c7ffbf539b9665df9b3064608f50cf7eee06cffede24f46aa7a4
5
5
  SHA512:
6
- metadata.gz: 48cf23b7cb6c1526825316f148875a7554371e7e3c2ca5716a3e33b7585277789a7d698d4b0811eb60c59d445af742e16b43a3c454d68365f372f428db245f0a
7
- data.tar.gz: 9b1d3b546e5a0ac019efb0f46fa2763b40614e0efb406b38e5430d96fbb8540d99b92d7f34fcafe86f19e34fd91b70319ea7240ad9b1c9ee88d1f55c1c021c8b
6
+ metadata.gz: 0f4e5a1e7f61304642cc758d82a4ec2a95af43a4f10fda93cb1f813468a1f6a6a321da5606acca79ad8724ca9ed79af162b8bedf82b3d9c0cae51ffd59a111de
7
+ data.tar.gz: 02712023f7dcd89f9083b95074ddde9770c6b73083e314d380bc71dd5020896a650ef509f79ff7183821f5ca18193805c2c1e9cc5a8cd65624ff5796bbe74214
data/bin/timestamp CHANGED
@@ -3,9 +3,77 @@
3
3
 
4
4
  require 'timestamp_maker'
5
5
  require 'optparse'
6
+ require 'optparse/time'
6
7
 
7
- option_parser = OptionParser.new do |opts|
8
- opts.banner = "Usage: #{__FILE__} INPUT_FILE_PATH OUTPUT_FILE_PATH"
8
+ options = {
9
+ format: '%Y-%m-%d %H:%M:%S',
10
+ time: nil,
11
+ time_zone: nil,
12
+ font_size: 32,
13
+ font_family: 'Sans',
14
+ font_color: 'white',
15
+ background_color: '#000000B3',
16
+ coordinate_origin: 'top-left',
17
+ x: 32,
18
+ y: 32,
19
+ font_padding: 8,
20
+ require: []
21
+ }
22
+
23
+ option_parser = OptionParser.new do |parser|
24
+ parser.banner = "Usage: #{__FILE__} [options] INPUT_FILE_PATH OUTPUT_FILE_PATH"
25
+
26
+ parser.on('-f FORMAT', '--format FORMAT', 'strftime() format string, defaults to "%Y-%m-%d %H:%M:%S".') do |value|
27
+ options[:format] = value
28
+ end
29
+
30
+ parser.on(
31
+ '-t TIME',
32
+ '--time TIME', Time,
33
+ 'ISO 8601 or RFC 2616 string. By default, retrieves from file\'s metadata'
34
+ ) do |value|
35
+ options[:time] = value
36
+ end
37
+
38
+ parser.on('--font-size NUMBER', Integer, 'Defaults to 32.') do |value|
39
+ options[:font_size] = value
40
+ end
41
+
42
+ parser.on('--font-family FONT_FAMILY', 'Defaults to "Sans"') do |value|
43
+ options[:font_family] = value
44
+ end
45
+
46
+ parser.on('--font-color COLOR', '"#RRGGBB[AA]" or color name, Defaults to "white"') do |value|
47
+ options[:font_color] = value
48
+ end
49
+
50
+ parser.on('--background-color COLOR', '"#RRGGBB[AA]" or color name, Defaults to "#000000B3"') do |value|
51
+ options[:background_color] = value
52
+ end
53
+
54
+ parser.on('--time-zone TIME_ZONE', 'IANA time zone. By default, retrieves from media file\'s metadata') do |value|
55
+ options[:time_zone] = value
56
+ end
57
+
58
+ parser.on('--coordinate-origin ORIGIN', 'Should be "[top|bottom]-[left|right]". Defaults to "top-left"') do |value|
59
+ options[:coordinate_origin] = value
60
+ end
61
+
62
+ parser.on('-x X', Integer, 'coordinate x. Defaults to 32.') do |value|
63
+ options[:x] = value
64
+ end
65
+
66
+ parser.on('-y Y', Integer, 'coordinate y, Defaults to 32.') do |value|
67
+ options[:y] = value
68
+ end
69
+
70
+ parser.on('--font-padding NUM', Integer, 'Defaults to 8.') do |value|
71
+ options[:font_padding] = value
72
+ end
73
+
74
+ parser.on('-r LIB', '--require LIB', Array, 'Comma-separated Ruby libs') do |value|
75
+ options[:require] = value
76
+ end
9
77
  end
10
78
  option_parser.parse!
11
79
 
@@ -17,4 +85,19 @@ end
17
85
  input = ARGV.shift
18
86
  output = ARGV.shift
19
87
 
20
- TimestampMaker.add_timestamp(input, output)
88
+ options[:require].each { |i| require i }
89
+ TimestampMaker.instance.add_timestamp(
90
+ input,
91
+ output,
92
+ format: options[:format],
93
+ time: options[:time],
94
+ time_zone: options[:time_zone],
95
+ font_size: options[:font_size],
96
+ font_family: options[:font_family],
97
+ font_color: options[:font_color],
98
+ background_color: options[:background_color],
99
+ coordinate_origin: options[:coordinate_origin],
100
+ x: options[:x],
101
+ y: options[:y],
102
+ font_padding: options[:font_padding]
103
+ )
@@ -1,28 +1,83 @@
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'
8
-
9
- module TimestampMaker
10
- @mime_recognizer = MimeRecognizer
11
- @image_timestamper = ImageTimestamper
12
- @video_timestamper = VideoTimestamper
13
-
14
- class << self
15
- attr_reader :mime_recognizer, :image_timestamper, :video_timestamper
16
-
17
- def add_timestamp(input_path, output_path)
18
- mime_type = mime_recognizer.recognize(input_path)
19
- processor =
20
- case mime_type.split('/').first
21
- when 'image' then image_timestamper
22
- when 'video' then video_timestamper
23
- else raise "Unsupported MIME type: ##{mime_type}"
24
- end
25
- processor.add_timestamp(input_path, output_path)
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
+ require 'tzinfo'
9
+
10
+ class TimestampMaker
11
+ COORDINATE_ORIGINS = %w[
12
+ top-left
13
+ top-right
14
+ bottom-left
15
+ bottom-right
16
+ ].freeze
17
+
18
+ attr_accessor :mime_recognizer, :handlers
19
+
20
+ singleton_class.attr_writer :instance
21
+ def self.instance
22
+ @instance ||= new
23
+ end
24
+
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
39
+
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?
57
+
58
+ time = handler.creation_time(input_path) if time.nil?
59
+ raise ArgumentError unless time.is_a?(Time)
60
+
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(',')}"
67
+ )
26
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
+ )
27
82
  end
28
83
  end
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'time'
5
+ require 'open3'
6
+ require 'English'
7
+ require 'tzinfo'
8
+
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
+
23
+ def add_timestamp(
24
+ input_path,
25
+ output_path,
26
+ time,
27
+ format:,
28
+ font_size:,
29
+ font_family:,
30
+ font_color:,
31
+ background_color:,
32
+ coordinate_origin:,
33
+ x:,
34
+ y:,
35
+ font_padding:
36
+ )
37
+ creation_timestamp = time.to_i
38
+ text = "%{pts:localtime:#{creation_timestamp}:#{escape_text_expansion_argument(format)}}"
39
+ drawtext = +"#{coord_map(coordinate_origin, x, y)}:" << %W[
40
+ font=#{escape_filter_description_value(font_family)}
41
+ fontsize=#{font_size}
42
+ fontcolor=#{font_color}
43
+ box=1
44
+ boxcolor=#{background_color}
45
+ boxborderw=#{font_padding}
46
+ text=#{escape_filter_description_value(text)}
47
+ ].join(':')
48
+
49
+ command = %W[
50
+ ffmpeg -y
51
+ -v warning
52
+ -i #{input_path}
53
+ -map_metadata 0
54
+ -vf drawtext=#{escape_filter_description(drawtext)}
55
+ #{output_path}
56
+ ]
57
+
58
+ tz = tz_env_string(time)
59
+ raise "Command failed with exit #{$CHILD_STATUS.exitstatus}: #{command.first}" unless system({ 'TZ' => tz },
60
+ *command)
61
+ end
62
+
63
+ def creation_time(input_path)
64
+ command = %W[
65
+ ffprobe -v warning -print_format json
66
+ -show_entries format_tags=creation_time,com.apple.quicktime.creationdate,location
67
+ #{input_path}
68
+ ]
69
+ stdout_string, status = Open3.capture2(*command)
70
+ raise unless status.success?
71
+
72
+ parsed = JSON.parse(stdout_string)
73
+ iso8601_string = parsed['format']['tags']['com.apple.quicktime.creationdate'] || parsed['format']['tags']['creation_time']
74
+ raise 'Cannot find creation time' if iso8601_string.nil?
75
+
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
88
+ end
89
+
90
+ private
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
+
99
+ def tz_env_string(time)
100
+ return time.zone.name if time.zone.is_a? TZInfo::Timezone
101
+
102
+ TZInfo::Timezone.get(time.zone).name
103
+ rescue TZInfo::InvalidTimezoneIdentifier
104
+ offset = time.utc_offset
105
+ tz_string = "#{offset / 3600}:#{offset % 3600 / 16}:#{offset % 60}"
106
+ return "+#{tz_string}" if offset.negative?
107
+
108
+ "-#{tz_string}"
109
+ end
110
+
111
+ def coord_map(coordinate_origin, x, y)
112
+ case coordinate_origin
113
+ when 'top-left' then "x=#{x}:y=#{y}"
114
+ when 'top-right' then "x=w-tw-#{x}:y=#{y}"
115
+ when 'bottom-left' then "x=#{x}:y=h-th-#{y}"
116
+ when 'bottom-right' then "x=w-tw-#{x}:y=h-th-#{y}"
117
+ end
118
+ end
119
+
120
+ def escape_text_expansion_argument(string)
121
+ string.gsub(/[:{}]/, '\\\\\\&')
122
+ end
123
+
124
+ def escape_filter_description_value(string)
125
+ string.gsub(/[:\\']/, '\\\\\\&')
126
+ end
127
+
128
+ def escape_filter_description(string)
129
+ string.gsub(/[\\'\[\],;]/, '\\\\\\&')
130
+ end
131
+ end
132
+ end
133
+ end
@@ -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.0.2
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-18 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
@@ -24,6 +24,34 @@ dependencies:
24
24
  - - "~>"
25
25
  - !ruby/object:Gem::Version
26
26
  version: '1.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: tzinfo
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
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
27
55
  - !ruby/object:Gem::Dependency
28
56
  name: minitest
29
57
  requirement: !ruby/object:Gem::Requirement
@@ -52,8 +80,8 @@ dependencies:
52
80
  - - "~>"
53
81
  - !ruby/object:Gem::Version
54
82
  version: '13.0'
55
- description: timestamp_maker is a command-line tool that adds timestamp to images
56
- and videos.
83
+ description: timestamp_maker is a command-line tool that adds timestamp on assets/videos
84
+ based on their creation time.
57
85
  email: tonytonyjan@gmail.com
58
86
  executables:
59
87
  - timestamp
@@ -62,9 +90,12 @@ extra_rdoc_files: []
62
90
  files:
63
91
  - bin/timestamp
64
92
  - lib/timestamp_maker.rb
65
- - lib/timestamp_maker/image_timestamper.rb
66
- - lib/timestamp_maker/mime_recognizer.rb
67
- - 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
68
99
  homepage: https://github.com/tonytonyjan/timestamp_maker
69
100
  licenses:
70
101
  - MIT
@@ -78,7 +109,10 @@ required_ruby_version: !ruby/object:Gem::Requirement
78
109
  requirements:
79
110
  - - ">="
80
111
  - !ruby/object:Gem::Version
81
- version: '0'
112
+ version: '2.5'
113
+ - - "<"
114
+ - !ruby/object:Gem::Version
115
+ version: '4'
82
116
  required_rubygems_version: !ruby/object:Gem::Requirement
83
117
  requirements:
84
118
  - - ">="
@@ -88,6 +122,6 @@ requirements: []
88
122
  rubygems_version: 3.2.22
89
123
  signing_key:
90
124
  specification_version: 4
91
- summary: timestamp_maker is a command-line tool that adds timestamp to images and
92
- videos.
125
+ summary: timestamp_maker is a command-line tool that adds timestamp on assets/videos
126
+ based on their creation time.
93
127
  test_files: []
@@ -1,46 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'open3'
4
- require 'time'
5
-
6
- module TimestampMaker
7
- module ImageTimestamper
8
- def self.add_timestamp(input_path, output_path)
9
- time_string = creation_time(input_path).iso8601
10
- command = %W[
11
- magick convert #{input_path}
12
- (
13
- -background rgba(0,0,0,0.7)
14
- -fill white
15
- -font Roboto
16
- -pointsize 32
17
- -gravity NorthWest
18
- -splice 1x1
19
- -gravity SouthEast
20
- -splice 1x1
21
- label:#{time_string}
22
- )
23
- -gravity NorthWest -geometry +32+32 -composite #{output_path}
24
- ]
25
- system(*command, exception: true)
26
- end
27
-
28
- def self.creation_time(input_path)
29
- command = %W[
30
- magick identify -format %[exif:DateTime*]%[exif:OffsetTime*] #{input_path}
31
- ]
32
-
33
- stdout_string, status = Open3.capture2(*command)
34
- raise unless status.success?
35
-
36
- parsed = Hash[stdout_string.split("\n").map!{ _1[5..].split('=') }]
37
-
38
- time_string = parsed['DateTimeOriginal'] || parsed['DateTimeDigitized'] || parsed['DateTime']
39
- raise 'Cannot find creation time' if time_string.nil?
40
-
41
- time_offset_string = parsed['OffsetTimeOriginal'] || parsed['OffsetTimeDigitized'] || parsed['OffsetTime'] || 'Z'
42
-
43
- Time.strptime("#{time_string} #{time_offset_string}", '%Y:%m:%d %H:%M:%S %Z')
44
- end
45
- end
46
- 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
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
- require 'time'
5
- require 'open3'
6
- require 'shellwords'
7
-
8
- module TimestampMaker
9
- module VideoTimestamper
10
- def self.add_timestamp(input_path, output_path)
11
- creation_timestamp = creation_time(input_path).to_i
12
- drawtext = %W[
13
- x=32
14
- y=32
15
- font=Roboto
16
- fontsize=32
17
- fontcolor=white
18
- box=1
19
- boxcolor=black@0.7
20
- boxborderw=8
21
- text=%{pts\\\\:localtime\\\\:#{creation_timestamp}}
22
- ].join(':')
23
-
24
- command = %W[
25
- ffmpeg -y -v warning -i #{input_path} -map_metadata 0
26
- -vf drawtext=#{drawtext}
27
- #{output_path}
28
- ]
29
-
30
- system(*command, exception: true)
31
- end
32
-
33
- def self.creation_time(input_path)
34
- command = %W[
35
- ffprobe -v warning -print_format json -show_entries format_tags=creation_time #{Shellwords.escape(input_path)}
36
- ]
37
- stdout_string, status = Open3.capture2(*command)
38
- raise unless status.success?
39
-
40
- parsed = JSON.parse(stdout_string)
41
- iso8601_string = parsed['format']['tags']['creation_time']
42
- raise 'Cannot find creation time' if iso8601_string.nil?
43
-
44
- Time.iso8601(iso8601_string)
45
- end
46
- end
47
- end