video2gif 0.0.33 → 0.0.34

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: e4bff1ae338db86be23755a9bdf6bb4e5a877b942f3a48c32af28317e35c8508
4
- data.tar.gz: d3aa41909f20f0ef78ca35fafa2efc4dc09932aa63372401a44efb65c3d315d2
3
+ metadata.gz: bc6e44b0ef580d23810cfaaae7449d4c0aa42e4e21a4a549f14cd549ed78a6e4
4
+ data.tar.gz: 229342c782db8a61f1e7706e13f00e25ad5a289bd796e9b2975482a16e2d8d31
5
5
  SHA512:
6
- metadata.gz: d2608410cbbf81ab35a88825a66b5b9f11bfa9cbe00f42fdafa9c6a5f112ce8070595dabe4cba2cec879a7133a13defbcd633d3706b0f004f8b614c0eec28582
7
- data.tar.gz: 2fe3759d6bc111f056b0f15f0c77c516d508186cedb5ca41a68281346b9428a0d0efa6ec8b1a101088d90f26990f0a41d308f51c47f4b6ee12d5e88a3753270a
6
+ metadata.gz: 18edb4ce95a5f30993cd2adddc22ade8bc1e135c322ad150bfa051590e46dc676b4ae5d611bc81addf1c521bc0143f755faa9381893120ccf894609d8635b69e
7
+ data.tar.gz: aeb1a6e3183f0e1c4b9c7fbac2015d8a8d289cf9f177b6e1ea18699301cd63e6c981ca2a2416fb9354b90fabb22363867c8c28d9e2b35dcdcffe2e743674fcc3
@@ -1,162 +1,185 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative 'ffmpeg/subtitles'
4
+
3
5
 
4
6
  module Video2gif
5
7
  module FFmpeg
8
+ include Subtitles
9
+
6
10
  CROP_REGEX = /crop=([0-9]+\:[0-9]+\:[0-9]+\:[0-9]+)/
7
11
 
8
- # TODO: This whole method needs to be broken up significantly.
9
- def self.filtergraph(options)
10
- filtergraph = []
12
+ def self.video_info(options)
13
+ options[:probe_infos][:streams].find { |s| s[:codec_type] == 'video' }
14
+ end
11
15
 
12
- # If we want subtitles and *have* subtitles, we need some info to
13
- # use them.
14
- video_info = options[:probe_infos][:streams].find { |s| s[:codec_type] == 'video' }
15
- if options[:subtitles] && options[:probe_infos][:streams].any? { |s| s[:codec_type] == 'subtitle' }
16
- subtitle_info = options[:probe_infos][:streams]
17
- .find_all { |s| s[:codec_type] == 'subtitle' }
18
- .fetch(options[:subtitle_index], nil)
19
- end
16
+ def self.rate(options)
17
+ "setpts=PTS/#{options[:rate]}" if options[:rate]
18
+ end
20
19
 
21
- # Bitmap formatted subtitles go first so that they get scaled
22
- # correctly.
23
- if options[:subtitles] &&
24
- options[:probe_infos][:streams].any? { |s| s[:codec_type] == 'subtitle' } &&
25
- Subtitles::KNOWN_BITMAP_FORMATS.include?(subtitle_info[:codec_name])
26
- filtergraph << "[0:s:#{options[:subtitle_index]}]scale=" + %W[
27
- flags=lanczos
28
- sws_dither=none
29
- width=#{video_info[:width]}
30
- height=#{video_info[:height]}
31
- ].join(':') + '[subs]'
32
- filtergraph << '[0:v][subs]overlay=format=auto'
33
- end
20
+ def self.interpolate(options)
21
+ if options[:rate] && Float(options[:rate]) < 1 # only interpolate slowed down video
22
+ minterpolate_parameters = []
23
+
24
+ minterpolate_parameters << 'mi_mode=mci'
25
+ minterpolate_parameters << 'mc_mode=aobmc'
26
+ minterpolate_parameters << 'me_mode=bidir'
27
+ minterpolate_parameters << 'me=epzs'
28
+ minterpolate_parameters << 'vsbmc=1'
29
+ minterpolate_parameters << "fps=#{video_info(options)[:avg_frame_rate] }/#{options[:rate]}"
34
30
 
35
- # Slow down or speed up video as early as possible so that we
36
- # don't end up trying to interpolate frames in that we already
37
- # dropped.
38
- if options[:rate]
39
- filtergraph << "setpts=PTS/#{options[:rate]}" if options[:rate]
40
- if Float(options[:rate]) < 1 # interpolate slowed down video
41
- minterpolate = []
42
- minterpolate << 'mi_mode=mci'
43
- minterpolate << 'mc_mode=aobmc'
44
- minterpolate << 'me_mode=bidir'
45
- minterpolate << 'me=epzs'
46
- minterpolate << 'vsbmc=1'
47
- minterpolate << "fps=#{video_info[:avg_frame_rate] }/#{options[:rate]}"
48
- filtergraph << 'minterpolate=' + minterpolate.join(':')
49
- end
31
+ 'minterpolate=' + minterpolate_parameters.join(':')
50
32
  end
33
+ end
51
34
 
52
- # Set 'fps' filter early, drop unneeded frames instead of
53
- # processing those.
54
- filtergraph << "fps=#{ options[:fps] || 10 }"
35
+ def self.rate_with_interpolation(options)
36
+ [
37
+ rate(options),
38
+ interpolate(options)
39
+ ]
40
+ end
55
41
 
56
- # Apply automatic cropping discovered during the cropdetect run.
57
- filtergraph << options[:autocrop] if options[:autocrop]
42
+ def self.fps(options)
43
+ "fps=#{ options[:fps] || 10 }"
44
+ end
58
45
 
59
- crop = []
60
- crop << "w=#{options[:wregion]}" if options[:wregion]
61
- crop << "h=#{options[:hregion]}" if options[:hregion]
62
- crop << "x=#{options[:xoffset]}" if options[:xoffset]
63
- crop << "y=#{options[:yoffset]}" if options[:yoffset]
64
- filtergraph << 'crop=' + crop.join(':') unless crop.empty?
46
+ def self.crop(options)
47
+ crop_parameters = []
65
48
 
66
- # Scale here before other filters to avoid unnecessary processing.
49
+ crop_parameters << "w=#{options[:wregion]}" if options[:wregion]
50
+ crop_parameters << "h=#{options[:hregion]}" if options[:hregion]
51
+ crop_parameters << "x=#{options[:xoffset]}" if options[:xoffset]
52
+ crop_parameters << "y=#{options[:yoffset]}" if options[:yoffset]
53
+
54
+ 'crop=' + crop_parameters.join(':') unless crop_parameters.empty?
55
+ end
56
+
57
+ def self.zscale(options)
58
+ zscale_parameters = []
59
+
60
+ zscale_parameters << 'dither=none'
61
+ zscale_parameters << 'filter=lanczos'
62
+ zscale_parameters << "width=#{ options[:width] || 400 }"
63
+ zscale_parameters << "height=trunc(#{ options[:width] || 400 }/dar)"
64
+
65
+ 'zscale=' + zscale_parameters.join(':')
66
+ end
67
+
68
+ def self.tonemap(options)
69
+ %W[
70
+ zscale=transfer=linear:npl=100
71
+ zscale=npl=100
72
+ format=gbrpf32le
73
+ zscale=primaries=bt709
74
+ tonemap=tonemap=#{options[:tonemap]}:desat=0
75
+ zscale=transfer=bt709:matrix=bt709:range=tv
76
+ format=yuv420p
77
+ ]
78
+ end
79
+
80
+ def self.zscale_and_tonemap(options)
67
81
  if options[:tonemap]
68
- # If we're attempting to convert HDR to SDR, use a set of
69
- # 'zscale' filters, 'format' filters, and the 'tonemap' filter.
70
- # The 'zscale' will do the resize for us as well.
71
- filtergraph << 'zscale=' + %W[
72
- dither=none
73
- filter=lanczos
74
- width=#{ options[:width] || 400 }
75
- height=trunc(#{ options[:width] || 400 }/dar)
76
- ].join(':')
77
- filtergraph << 'zscale=transfer=linear:npl=100'
78
- filtergraph << 'zscale=npl=100'
79
- filtergraph << 'format=gbrpf32le'
80
- filtergraph << 'zscale=primaries=bt709'
81
- filtergraph << "tonemap=tonemap=#{options[:tonemap]}:desat=0"
82
- filtergraph << 'zscale=transfer=bt709:matrix=bt709:range=tv'
83
- filtergraph << 'format=yuv420p'
84
- else
85
- # If we're not attempting to convert HDR to SDR, the standard
86
- # 'scale' filter is preferred (if we're resizing at all).
87
- filtergraph << 'scale=' + %W[
88
- flags=lanczos
89
- sws_dither=none
90
- width=#{ options[:width] || 400 }
91
- height=trunc(#{ options[:width] || 400 }/dar)
92
- ].join(':') unless options[:tonemap]
82
+ [
83
+ zscale(options),
84
+ tonemap(options)
85
+ ]
93
86
  end
87
+ end
88
+
89
+ def self.scale(options)
90
+ unless options[:tonemap]
91
+ scale_parameters = []
92
+
93
+ scale_parameters << 'flags=lanczos'
94
+ scale_parameters << 'sws_dither=none'
95
+ scale_parameters << "width=#{ options[:width] || 400 }"
96
+ scale_parameters << "height=trunc(#{ options[:width] || 400 }/dar)"
94
97
 
95
- # Perform any desired equalization before we overlay text so that
96
- # it won't be affected.
97
- filtergraph << "eq=contrast=#{options[:contrast]}" if options[:contrast]
98
- filtergraph << "eq=brightness=#{options[:brightness]}" if options[:brightness]
99
- filtergraph << "eq=saturation=#{options[:saturation]}" if options[:saturation]
100
- filtergraph << "eq=gamma=#{options[:gamma]}" if options[:gamma]
101
- filtergraph << "eq=gamma_r=#{options[:gamma_r]}" if options[:gamma_r]
102
- filtergraph << "eq=gamma_g=#{options[:gamma_g]}" if options[:gamma_g]
103
- filtergraph << "eq=gamma_b=#{options[:gamma_b]}" if options[:gamma_b]
104
-
105
- # Embed text subtitles later so that they don't get processed by
106
- # cropping, etc., which might accidentally crop them out.
107
- if options[:subtitles] &&
108
- options[:probe_infos][:streams].any? { |s| s[:codec_type] == 'subtitle' } &&
109
- Subtitles::KNOWN_TEXT_FORMATS.include?(subtitle_info[:codec_name])
110
- filtergraph << "setpts=PTS+#{Utils.duration_to_seconds(options[:seek])}/TB"
111
- filtergraph << "subtitles='#{options[:input_filename]}':si=#{options[:subtitle_index]}"
112
- filtergraph << 'setpts=PTS-STARTPTS'
98
+ 'scale=' + scale_parameters.join(':')
113
99
  end
100
+ end
101
+
102
+ def self.eq(options)
103
+ eq_parameters = []
104
+
105
+ eq_parameters << "contrast=#{options[:contrast]}" if options[:contrast]
106
+ eq_parameters << "brightness=#{options[:brightness]}" if options[:brightness]
107
+ eq_parameters << "saturation=#{options[:saturation]}" if options[:saturation]
108
+ eq_parameters << "gamma=#{options[:gamma]}" if options[:gamma]
109
+ eq_parameters << "gamma_r=#{options[:gamma_r]}" if options[:gamma_r]
110
+ eq_parameters << "gamma_g=#{options[:gamma_g]}" if options[:gamma_g]
111
+ eq_parameters << "gamma_b=#{options[:gamma_b]}" if options[:gamma_b]
112
+
113
+ 'eq=' + eq_parameters.join(":")
114
+ end
115
+
116
+ def self.text(options)
117
+ options[:text].gsub(/\\n/, ' ')
118
+ .gsub(/([:])/, '\\\\\\\\\\1')
119
+ .gsub(/([,])/, '\\\\\\1')
120
+ .gsub(/\b'\b/, "\u2019")
121
+ .gsub(/\B"\b([^"\u201C\u201D\u201E\u201F\u2033\u2036\r\n]+)\b?"\B/, "\u201C\\1\u201D")
122
+ .gsub(/\B'\b([^'\u2018\u2019\u201A\u201B\u2032\u2035\r\n]+)\b?'\B/, "\u2018\\1\u2019")
123
+ end
114
124
 
115
- # If there is text to superimpose, do it here before palette
116
- # generation to ensure the color looks appropriate.
125
+ def self.drawtext(options)
117
126
  if options[:text]
118
127
  count_of_lines = options[:text].scan(/\\n/).count + 1
119
- text = options[:text]
120
- .gsub(/\\n/, ' ')
121
- .gsub(/([:])/, '\\\\\\\\\\1')
122
- .gsub(/([,])/, '\\\\\\1')
123
- .gsub(/\b'\b/, "\u2019")
124
- .gsub(/\B"\b([^"\u201C\u201D\u201E\u201F\u2033\u2036\r\n]+)\b?"\B/, "\u201C\\1\u201D")
125
- .gsub(/\B'\b([^'\u2018\u2019\u201A\u201B\u2032\u2035\r\n]+)\b?'\B/, "\u2018\\1\u2019")
126
-
127
- filtergraph << 'drawtext=' + %W[
128
- x='#{ options[:xpos] || '(main_w/2-text_w/2)' }'
129
- y='#{ options[:ypos] || "(main_h-line_h*1.5*#{count_of_lines})" }'
130
- fontsize='#{ options[:textsize] || 32 }'
131
- fontcolor='#{ options[:textcolor] || 'white' }'
132
- borderw='#{ options[:textborder] || 1 }'
133
- fontfile='#{ options[:textfont] || 'Arial'}'\\\\:style='#{options[:textvariant] || 'Bold' }'
134
- text='#{text}'
135
- ].join(':')
128
+
129
+ drawtext_parameters = []
130
+ drawtext_parameters << "x='#{ options[:xpos] || '(main_w/2-text_w/2)' }'"
131
+ drawtext_parameters << "y='#{ options[:ypos] || "(main_h-line_h*1.5*#{count_of_lines})" }'"
132
+ drawtext_parameters << "fontsize='#{ options[:textsize] || 32 }'"
133
+ drawtext_parameters << "fontcolor='#{ options[:textcolor] || 'white' }'"
134
+ drawtext_parameters << "borderw='#{ options[:textborder] || 1 }'"
135
+ drawtext_parameters << "fontfile='#{ options[:textfont] || 'Arial'}'\\\\:style='#{options[:textvariant] || 'Bold' }'"
136
+ drawtext_parameters << "text='#{text(options)}'"
137
+
138
+ 'drawtext=' + drawtext_parameters.join(':')
136
139
  end
140
+ end
141
+
142
+ def self.split
143
+ 'split[palettegen][paletteuse]'
144
+ end
137
145
 
138
- # Split the stream into two copies, labeled with output pads for
139
- # the palettegen/paletteuse filters to use.
140
- filtergraph << 'split[palettegen][paletteuse]'
141
-
142
- # Using a copy of the stream created above labeled "palettegen",
143
- # generate a palette from the stream using the specified number of
144
- # colors and optimizing for moving objects in the stream. Label
145
- # this stream's output as "palette."
146
- filtergraph << '[palettegen]palettegen=' + %W[
147
- #{options[:palette] || 256}
148
- stats_mode=#{options[:palettemode] || 'diff'}
149
- ].join(':') + '[palette]'
150
-
151
- # Using a copy of the stream from the 'split' filter and the
152
- # generated palette as inputs, apply the final palette to the GIF.
153
- # For non-moving parts of the GIF, attempt to reuse the same
154
- # palette from frame to frame.
155
- filtergraph << '[paletteuse][palette]paletteuse=' + %W[
156
- dither=#{options[:dither] || 'floyd_steinberg'}
157
- diff_mode=rectangle
158
- #{options[:palettemode] == 'single' ? 'new=1' : ''}
159
- ].join(':')
146
+ def self.palettegen(options)
147
+ palettegen_parameters = []
148
+
149
+ palettegen_parameters << "#{ options[:palette] || 256 }"
150
+ palettegen_parameters << "stats_mode=#{options[:palettemode] || 'diff'}"
151
+
152
+ '[palettegen]palettegen=' + palettegen_parameters.join(':') + '[palette]'
153
+ end
154
+
155
+ def self.paletteuse(options)
156
+ paletteuse_parameters = []
157
+
158
+ paletteuse_parameters << "dither=#{options[:dither] || 'floyd_steinberg'}"
159
+ paletteuse_parameters << 'diff_mode=rectangle'
160
+ paletteuse_parameters << "#{options[:palettemode] == 'single' ? 'new=1' : ''}"
161
+
162
+ '[paletteuse][palette]paletteuse=' + paletteuse_parameters.join(':')
163
+ end
164
+
165
+ def self.filtergraph(options)
166
+ filtergraph = []
167
+
168
+ filtergraph << bitmap_subtitles_scale_overlay(options)
169
+ filtergraph << rate_with_interpolation(options)
170
+ filtergraph << fps(options)
171
+ filtergraph << options[:autocrop] if options[:autocrop]
172
+ filtergraph << crop(options)
173
+ filtergraph << zscale_and_tonemap(options)
174
+ filtergraph << scale(options)
175
+ filtergraph << eq(options)
176
+ filtergraph << text_subtitles(options)
177
+ filtergraph << drawtext(options)
178
+ filtergraph << split
179
+ filtergraph << palettegen(options)
180
+ filtergraph << paletteuse(options)
181
+
182
+ filtergraph.flatten.compact
160
183
  end
161
184
 
162
185
  def self.ffprobe_command(options, logger, executable: 'ffprobe')
@@ -26,15 +26,73 @@ module Video2gif
26
26
  ttml
27
27
  vplayer
28
28
  webvtt
29
- ]
29
+ ].freeze
30
30
 
31
31
  KNOWN_BITMAP_FORMATS = %w[
32
32
  dvb_subtitle
33
33
  dvd_subtitle
34
34
  hdmv_pgs_subtitle
35
35
  xsub
36
- ]
36
+ ].freeze
37
+
38
+ def self.included(m)
39
+ m.extend(ClassMethods)
40
+ end
41
+
42
+ module ClassMethods
43
+ def has_subtitles(options)
44
+ options[:subtitles] && options[:probe_infos][:streams].any? { |s| s[:codec_type] == 'subtitle' }
45
+ end
46
+
47
+ def subtitle_info(options)
48
+ if has_subtitles(options)
49
+ options[:probe_infos][:streams].find_all { |s| s[:codec_type] == 'subtitle' }
50
+ .fetch(options[:subtitle_index], nil)
51
+ end
52
+ end
53
+
54
+ def has_bitmap_subtitles(options)
55
+ has_subtitles(options) && KNOWN_BITMAP_FORMATS.include?(subtitle_info(options)[:codec_name])
56
+ end
57
+
58
+ def has_text_subtitles(options)
59
+ has_subtitles(options) && KNOWN_TEXT_FORMATS.include?(subtitle_info(options)[:codec_name])
60
+ end
61
+
62
+ def subtitles_scale(options)
63
+ scale_parameters = []
64
+
65
+ scale_parameters << 'flags=lanczos'
66
+ scale_parameters << 'sws_dither=none'
67
+ scale_parameters << "width=#{video_info(options)[:width]}"
68
+ scale_parameters << "height=#{video_info(options)[:height]}"
69
+
70
+ "[0:s:#{options[:subtitle_index]}]scale=#{scale_parameters.join(':')}[subs]"
71
+ end
72
+
73
+ def subtitles_overlay
74
+ '[0:v][subs]overlay=format=auto'
75
+ end
76
+
77
+ def bitmap_subtitles_scale_overlay(options)
78
+ if has_bitmap_subtitles(options)
79
+ [
80
+ subtitles_scale(options),
81
+ subtitles_overlay
82
+ ]
83
+ end
84
+ end
85
+
86
+ def text_subtitles(options)
87
+ if has_text_subtitles(options)
88
+ %W[
89
+ setpts=PTS+#{Utils.duration_to_seconds(options[:seek])}/TB
90
+ subtitles='#{options[:input_filename]}':si=#{options[:subtitle_index]}
91
+ setpts=PTS-STARTPTS
92
+ ]
93
+ end
94
+ end
95
+ end
37
96
  end
38
97
  end
39
98
  end
40
-
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Video2gif
4
- VERSION = '0.0.33'
4
+ VERSION = '0.0.34'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: video2gif
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.33
4
+ version: 0.0.34
5
5
  platform: ruby
6
6
  authors:
7
7
  - Emily St.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-06-14 00:00:00.000000000 Z
11
+ date: 2019-07-30 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler