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 +4 -4
- data/bin/timestamp +86 -3
- data/lib/timestamp_maker.rb +77 -22
- data/lib/timestamp_maker/handlers/ffmpeg.rb +133 -0
- data/lib/timestamp_maker/handlers/image_magick.rb +113 -0
- data/lib/timestamp_maker/mime_recognizers/marcel.rb +13 -0
- data/lib/timestamp_maker/time_zone_lookupers/geo_name.rb +30 -0
- data/lib/timestamp_maker/time_zone_lookupers/mock.rb +15 -0
- data/lib/timestamp_maker/time_zone_lookupers/wheretz.rb +18 -0
- metadata +44 -10
- data/lib/timestamp_maker/image_timestamper.rb +0 -46
- data/lib/timestamp_maker/mime_recognizer.rb +0 -9
- data/lib/timestamp_maker/video_timestamper.rb +0 -47
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e7f96f255213ceac42648b8ad1317b745d64062f6ad116fd8365d64408077101
|
4
|
+
data.tar.gz: 6c4c191ee701c7ffbf539b9665df9b3064608f50cf7eee06cffede24f46aa7a4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
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
|
-
|
8
|
-
|
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
|
-
|
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
|
+
)
|
data/lib/timestamp_maker.rb
CHANGED
@@ -1,28 +1,83 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'marcel'
|
4
3
|
require 'pathname'
|
5
|
-
require 'timestamp_maker/
|
6
|
-
require 'timestamp_maker/
|
7
|
-
require 'timestamp_maker/
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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,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,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.
|
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-
|
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
|
56
|
-
|
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/
|
66
|
-
- lib/timestamp_maker/
|
67
|
-
- lib/timestamp_maker/
|
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: '
|
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
|
92
|
-
|
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,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
|