video2gif 0.0.33 → 0.0.34

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: 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