video2gif 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: a792d0bc3ded611a4295f7195ab17fbe5a71c65ae84bc527eb02c9f6c667088c
4
+ data.tar.gz: f17cfefa5ef240925a2915ae089df6c7d9eacd7bf2d1ac449bce6f707a2a15c7
5
+ SHA512:
6
+ metadata.gz: ba5e69371e4edb486ff79a7ffa539cbf656c3edf1bc1794f58a02add3240fd81acadaaba8502b123990e09efa1d630560b4a20357d747f3349b3f9314864a9aa
7
+ data.tar.gz: 243ddc56a97b5470d7771e90731dafbb9d0c69c2427e406a4f6f664305fe6d1bd40b27cf29b8a99221dabcce6d76a562ea5321767e161cdf72be165a2a2f94a3
data/.gitignore ADDED
@@ -0,0 +1,11 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.6.1
7
+ before_install: gem install bundler -v 2.0.1
data/CONTRIBUTING.md ADDED
@@ -0,0 +1,26 @@
1
+ Contributing
2
+ ============
3
+
4
+ Performance improvements, bug fixes, and compatibility improvements are
5
+ welcome, provided that the change matches the style of the rest of the
6
+ plugin, does not introduce needless complexity, and complies with the
7
+ following guidelines for commits and pull requests.
8
+
9
+
10
+ Commits and Pull Requests
11
+ -------------------------
12
+
13
+ Each commit should contain a single, discrete change which does not
14
+ break when applied in isolation. It should be possible to use
15
+ `git-bisect(1)` on your commits. Do not use merge commits.
16
+
17
+ Each commit message should begin with a title of about fifty characters
18
+ in length, written in the active tense, imperative mood describing the
19
+ change briefly, followed by a blank line, followed by a longer
20
+ description of the change hard-wrapped to seventy-two columns or less.
21
+ The description should state why the change was necessary.
22
+
23
+ A pull request may have multiple commits if necessary. Each pull request
24
+ should address a single problem. It should describe the problem it
25
+ addresses and how it intends to solve it. The commits for that pull
26
+ request should solve the problem.
data/Gemfile ADDED
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in video2gif.gemspec
6
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,41 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ video2gif (0.1.0)
5
+
6
+ GEM
7
+ remote: https://rubygems.org/
8
+ specs:
9
+ coderay (1.1.2)
10
+ diff-lcs (1.3)
11
+ method_source (0.9.2)
12
+ pry (0.12.2)
13
+ coderay (~> 1.1.0)
14
+ method_source (~> 0.9.0)
15
+ rake (10.5.0)
16
+ rspec (3.8.0)
17
+ rspec-core (~> 3.8.0)
18
+ rspec-expectations (~> 3.8.0)
19
+ rspec-mocks (~> 3.8.0)
20
+ rspec-core (3.8.0)
21
+ rspec-support (~> 3.8.0)
22
+ rspec-expectations (3.8.2)
23
+ diff-lcs (>= 1.2.0, < 2.0)
24
+ rspec-support (~> 3.8.0)
25
+ rspec-mocks (3.8.0)
26
+ diff-lcs (>= 1.2.0, < 2.0)
27
+ rspec-support (~> 3.8.0)
28
+ rspec-support (3.8.0)
29
+
30
+ PLATFORMS
31
+ ruby
32
+
33
+ DEPENDENCIES
34
+ bundler (~> 2.0)
35
+ pry
36
+ rake (~> 10.0)
37
+ rspec (~> 3.0)
38
+ video2gif!
39
+
40
+ BUNDLED WITH
41
+ 2.0.1
data/LICENSE.txt ADDED
@@ -0,0 +1,125 @@
1
+ https://creativecommons.org/publicdomain/zero/1.0/legalcode
2
+
3
+ CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
4
+ LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
5
+ ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
6
+ INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
7
+ REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
8
+ PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
9
+ THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
10
+ HEREUNDER.
11
+
12
+
13
+ Statement of Purpose
14
+
15
+ The laws of most jurisdictions throughout the world automatically confer
16
+ exclusive Copyright and Related Rights (defined below) upon the creator
17
+ and subsequent owner(s) (each and all, an "owner") of an original work
18
+ of authorship and/or a database (each, a "Work").
19
+
20
+ Certain owners wish to permanently relinquish those rights to a Work for
21
+ the purpose of contributing to a commons of creative, cultural and
22
+ scientific works ("Commons") that the public can reliably and without
23
+ fear of later claims of infringement build upon, modify, incorporate in
24
+ other works, reuse and redistribute as freely as possible in any form
25
+ whatsoever and for any purposes, including without limitation commercial
26
+ purposes. These owners may contribute to the Commons to promote the
27
+ ideal of a free culture and the further production of creative, cultural
28
+ and scientific works, or to gain reputation or greater distribution for
29
+ their Work in part through the use and efforts of others.
30
+
31
+ For these and/or other purposes and motivations, and without any
32
+ expectation of additional consideration or compensation, the person
33
+ associating CC0 with a Work (the "Affirmer"), to the extent that he or
34
+ she is an owner of Copyright and Related Rights in the Work, voluntarily
35
+ elects to apply CC0 to the Work and publicly distribute the Work under
36
+ its terms, with knowledge of his or her Copyright and Related Rights in
37
+ the Work and the meaning and intended legal effect of CC0 on those
38
+ rights.
39
+
40
+ 1. Copyright and Related Rights. A Work made available under CC0 may
41
+ be protected by copyright and related or neighboring rights
42
+ ("Copyright and Related Rights"). Copyright and Related Rights
43
+ include, but are not limited to, the following:
44
+
45
+ i. the right to reproduce, adapt, distribute, perform, display,
46
+ communicate, and translate a Work;
47
+ ii. moral rights retained by the original author(s) and/or
48
+ performer(s);
49
+ iii. publicity and privacy rights pertaining to a person's image or
50
+ likeness depicted in a Work;
51
+ iv. rights protecting against unfair competition in regards to a
52
+ Work, subject to the limitations in paragraph 4(a), below;
53
+ v. rights protecting the extraction, dissemination, use and reuse
54
+ of data in a Work;
55
+ vi. database rights (such as those arising under Directive 96/9/EC
56
+ of the European Parliament and of the Council of 11 March 1996
57
+ on the legal protection of databases, and under any national
58
+ implementation thereof, including any amended or successor
59
+ version of such directive); and
60
+ vii. other similar, equivalent or corresponding rights throughout
61
+ the world based on applicable law or treaty, and any national
62
+ implementations thereof.
63
+
64
+ 2. Waiver. To the greatest extent permitted by, but not in contravention
65
+ of, applicable law, Affirmer hereby overtly, fully, permanently,
66
+ irrevocably and unconditionally waives, abandons, and surrenders all
67
+ of Affirmer's Copyright and Related Rights and associated claims and
68
+ causes of action, whether now known or unknown (including existing as
69
+ well as future claims and causes of action), in the Work (i) in all
70
+ territories worldwide, (ii) for the maximum duration provided by
71
+ applicable law or treaty (including future time extensions), (iii) in
72
+ any current or future medium and for any number of copies, and (iv)
73
+ for any purpose whatsoever, including without limitation commercial,
74
+ advertising or promotional purposes (the "Waiver"). Affirmer makes
75
+ the Waiver for the benefit of each member of the public at large and
76
+ to the detriment of Affirmer's heirs and successors, fully intending
77
+ that such Waiver shall not be subject to revocation, rescission,
78
+ cancellation, termination, or any other legal or equitable action to
79
+ disrupt the quiet enjoyment of the Work by the public as contemplated
80
+ by Affirmer's express Statement of Purpose.
81
+
82
+ 3. Public License Fallback. Should any part of the Waiver for any reason
83
+ be judged legally invalid or ineffective under applicable law, then
84
+ the Waiver shall be preserved to the maximum extent permitted taking
85
+ into account Affirmer's express Statement of Purpose. In addition, to
86
+ the extent the Waiver is so judged Affirmer hereby grants to each
87
+ affected person a royalty-free, non transferable, non sublicensable,
88
+ non exclusive, irrevocable and unconditional license to exercise
89
+ Affirmer's Copyright and Related Rights in the Work (i) in all
90
+ territories worldwide, (ii) for the maximum duration provided by
91
+ applicable law or treaty (including future time extensions), (iii) in
92
+ any current or future medium and for any number of copies, and (iv)
93
+ for any purpose whatsoever, including without limitation commercial,
94
+ advertising or promotional purposes (the "License"). The License
95
+ shall be deemed effective as of the date CC0 was applied by Affirmer
96
+ to the Work. Should any part of the License for any reason be judged
97
+ legally invalid or ineffective under applicable law, such partial
98
+ invalidity or ineffectiveness shall not invalidate the remainder of
99
+ the License, and in such case Affirmer hereby affirms that he or she
100
+ will not (i) exercise any of his or her remaining Copyright and
101
+ Related Rights in the Work or (ii) assert any associated claims and
102
+ causes of action with respect to the Work, in either case contrary to
103
+ Affirmer's express Statement of Purpose.
104
+
105
+ 4. Limitations and Disclaimers.
106
+
107
+ a. No trademark or patent rights held by Affirmer are waived,
108
+ abandoned, surrendered, licensed or otherwise affected by this
109
+ document.
110
+ b. Affirmer offers the Work as-is and makes no representations or
111
+ warranties of any kind concerning the Work, express, implied,
112
+ statutory or otherwise, including without limitation warranties of
113
+ title, merchantability, fitness for a particular purpose, non
114
+ infringement, or the absence of latent or other defects, accuracy,
115
+ or the present or absence of errors, whether or not discoverable,
116
+ all to the greatest extent permissible under applicable law.
117
+ c. Affirmer disclaims responsibility for clearing rights of other
118
+ persons that may apply to the Work or any use thereof, including
119
+ without limitation any person's Copyright and Related Rights in the
120
+ Work. Further, Affirmer disclaims responsibility for obtaining any
121
+ necessary consents, permissions or other rights required for any
122
+ use of the Work.
123
+ d. Affirmer understands and acknowledges that Creative Commons is not
124
+ a party to this document and has no duty or obligation with respect
125
+ to this CC0 or use of the Work.
data/README.md ADDED
@@ -0,0 +1,63 @@
1
+ video2gif
2
+ =========
3
+
4
+ `video2gif` eases converting any video into a GIF.
5
+
6
+ It uses [FFMpeg] and optionally [ImageMagick], so it understands any
7
+ video that [FFMpeg] does. It has an array of options to allow you to
8
+ select the part of the video you want, crop it automatically, overlay
9
+ text, and manipulate the color and brightness.
10
+
11
+
12
+ Installation
13
+ ------------
14
+
15
+ `video2gif` requires a recent version of [FFMpeg] installed and
16
+ available in the system `$PATH`. If you can run `ffmpeg` from the
17
+ command line, you're probably good. If not, use your favorite package
18
+ manager to install it.
19
+
20
+ If you install [ImageMagick], further optimization will automatically
21
+ take place on the resulting GIF.
22
+
23
+ Note that some features may not be available by default. For example,
24
+ tonemapping (used for HDR videos) requires `libzimg` support, not
25
+ included by default in the [FFMpeg] supplied by [Homebrew].
26
+
27
+ `video2gif` also requires Ruby and the ability to install a new gem. If
28
+ you have this available, run the following command to install it.
29
+
30
+ gem install video2gif
31
+
32
+
33
+ Usage
34
+ -----
35
+
36
+ The general syntax for the command follows.
37
+
38
+ video2gif <input video> [-o <output filename>] [<options>]
39
+
40
+ Use `video2gif --help` to see all the options available. Given an input
41
+ video, `video2gif` has a reasonable set of defaults to output a GIF of
42
+ the same size and with the same name in the same directory. However,
43
+ using the options available, you can change the output filename and the
44
+ appearance of the resulting GIF.
45
+
46
+ _Further documentation to come._
47
+
48
+
49
+ License
50
+ -------
51
+
52
+ This gem is released into the public domain (CC0 license). For details,
53
+ see: https://creativecommons.org/publicdomain/zero/1.0/legalcode
54
+
55
+
56
+ Contributing
57
+ ------------
58
+
59
+ To contribute to this plugin, find it on GitHub. Please see the
60
+ [CONTRIBUTING](CONTRIBUTING.markdown) file accompanying it for
61
+ guidelines.
62
+
63
+ https://github.com/emilyst/video2gif
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'video2gif'
5
+
6
+ # You can add fixtures and/or initialization code here to make
7
+ # experimenting with your gem easier. You can also use a different
8
+ # console, if you like.
9
+
10
+ require 'pry'
11
+ Pry.start
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
data/exe/video2gif ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'video2gif'
4
+
5
+ Video2gif.run
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Video2gif
4
+ VERSION = '0.0.1'
5
+ end
data/lib/video2gif.rb ADDED
@@ -0,0 +1,439 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'optparse'
4
+ require 'open3'
5
+ require 'logger'
6
+
7
+ require 'video2gif/version'
8
+
9
+
10
+ module Video2gif
11
+ def self.is_executable?(command)
12
+ ENV['PATH'].split(File::PATH_SEPARATOR).map do |path|
13
+ (ENV['PATHEXT'] ? ENV['PATHEXT'].split(';') : ['']).map do |extension|
14
+ File.executable?(File.join(path, "#{command}#{extension}"))
15
+ end
16
+ end.flatten.any?
17
+ end
18
+
19
+ def self.parse_args(args, logger)
20
+ options = {}
21
+
22
+ parser = OptionParser.new do |parser|
23
+ parser.banner = 'Usage: video2gif <video> [options] [<output GIF filename>]'
24
+ parser.separator ''
25
+ parser.separator 'General GIF options:'
26
+
27
+ parser.on('-s SEEK',
28
+ '--seek SEEK',
29
+ 'Set time to seek to in the input video (use a count of',
30
+ 'seconds or HH:MM:SS.SS format)') do |s|
31
+ options[:seek] = s
32
+ end
33
+
34
+ parser.on('-t TIME',
35
+ '--time TIME',
36
+ 'Set duration to use from the input video (use a count of',
37
+ 'seconds)') do |t|
38
+ options[:time] = t
39
+ end
40
+
41
+ parser.on('-f FRAMES',
42
+ '--fps FRAMES',
43
+ 'Set frames per second for the resulting GIF') do |f|
44
+ options[:fps] = f
45
+ end
46
+
47
+ parser.on('-w WIDTH',
48
+ '--width WIDTH',
49
+ 'Scale the width of the resulting GIF in pixels (aspect',
50
+ 'ratio is preserved)') do |w|
51
+ options[:width] = w
52
+ end
53
+
54
+ # parser.on('-hHEIGHT', '--height=HEIGHT', 'Scale the height of the resulting GIF') do |h|
55
+ # options[:height] = h
56
+ # end
57
+
58
+ parser.on('-p PALETTE',
59
+ '--palette PALETTE',
60
+ 'Set the palette size of the resulting GIF (maximum of 255',
61
+ 'colors)') do |p|
62
+ options[:palette] = p
63
+ end
64
+
65
+ parser.on('-c SIZE',
66
+ '--crop-size-w SIZE',
67
+ 'Pixel size of width to select from source video, before scaling') do |s|
68
+ options[:wregion] = s
69
+ end
70
+
71
+ parser.on('-h SIZE',
72
+ '--crop-size-h SIZE',
73
+ 'Pixel size of height to select from source video, before scaling') do |s|
74
+ options[:hregion] = s
75
+ end
76
+
77
+ parser.on('-x OFFSET',
78
+ '--crop-offset-x OFFSET',
79
+ 'Pixel offset from left to select from source video, before scaling') do |o|
80
+ options[:xoffset] = o
81
+ end
82
+
83
+ parser.on('-y OFFSET',
84
+ '--crop-offset-y OFFSET',
85
+ 'Pixel offset from top to select from source video, before scaling') do |o|
86
+ options[:yoffset] = o
87
+ end
88
+
89
+ parser.on('-d [THRESHOLD]',
90
+ '--crop-detect [THRESHOLD]',
91
+ 'Attempt automatic cropping based on black region, scaled',
92
+ 'from 0 (nothing) to 255 (everything), default threshold 24') do |c|
93
+ options[:cropdetect] = c || 24
94
+ end
95
+
96
+ parser.on('-o',
97
+ '--[no-]optimize',
98
+ 'Attempt to optimize GIF size with ImageMagick (default yes if available)') do |o|
99
+ options[:optimize] = o
100
+ end
101
+
102
+ parser.on('--contrast CONTRAST',
103
+ 'Apply contrast adjustment, scaled from -2.0 to 2.0 (default 1)') do |c|
104
+ options[:contrast] = c
105
+ options[:eq] = true
106
+ end
107
+
108
+ parser.on('--brightness BRIGHTNESS',
109
+ 'Apply brightness adjustment, scaled from -1.0 to 1.0 (default 0)') do |b|
110
+ options[:brightness] = b
111
+ options[:eq] = true
112
+ end
113
+
114
+ parser.on('--saturation SATURATION',
115
+ 'Apply saturation adjustment, scaled from 0.0 to 3.0 (default 1)') do |s|
116
+ options[:saturation] = s
117
+ options[:eq] = true
118
+ end
119
+
120
+ parser.on('--gamma GAMMA',
121
+ 'Apply gamma adjustment, scaled from 0.1 to 10.0 (default 1)') do |g|
122
+ options[:gamma] = g
123
+ options[:eq] = true
124
+ end
125
+
126
+ parser.on('--red-gamma GAMMA',
127
+ 'Apply red channel gamma adjustment, scaled from 0.1 to 10.0 (default 1)') do |g|
128
+ options[:gamma_r] = g
129
+ options[:eq] = true
130
+ end
131
+
132
+ parser.on('--green-gamma GAMMA',
133
+ 'Apply green channel gamma adjustment, scaled from 0.1 to 10.0 (default 1)') do |g|
134
+ options[:gamma_g] = g
135
+ options[:eq] = true
136
+ end
137
+
138
+ parser.on('--blue-gamma GAMMA',
139
+ 'Apply blue channel gamma adjustment, scaled from 0.1 to 10.0 (default 1)') do |g|
140
+ options[:gamma_b] = g
141
+ options[:eq] = true
142
+ end
143
+
144
+ parser.on('--tonemap [ALGORITHM]',
145
+ 'Attempt to force tonemapping from HDR (BT.2020) to SDR',
146
+ '(BT.709) using algorithm (experimental, requires ffmpeg with',
147
+ 'libzimg) (default "hable", "mobius" is a good alternative)') do |t|
148
+ options[:tonemap] = t || 'hable'
149
+ end
150
+
151
+ parser.separator ''
152
+ parser.separator 'Text overlay options (only used if text is defined):'
153
+
154
+ parser.on('-T TEXT',
155
+ '--text TEXT',
156
+ 'Set text to overlay on the GIF (use "\n" for line breaks)') do |p|
157
+ options[:text] = p
158
+ end
159
+
160
+ parser.on('-C TEXTCOLOR',
161
+ '--text-color TEXTCOLOR',
162
+ 'Set the color for text overlay') do |p|
163
+ options[:textcolor] = p
164
+ end
165
+
166
+ parser.on('-S TEXTSIZE',
167
+ '--text-size TEXTSIZE',
168
+ 'Set the point size for text overlay') do |p|
169
+ options[:textsize] = p
170
+ end
171
+
172
+ parser.on('-B TEXTBORDER',
173
+ '--text-border TEXTBORDER',
174
+ 'Set the width of the border for text overlay') do |p|
175
+ options[:textborder] = p
176
+ end
177
+
178
+ parser.on('-F TEXTFONT',
179
+ '--text-font TEXTFONT',
180
+ 'Set the font name for text overlay') do |p|
181
+ options[:textfont] = p
182
+ end
183
+
184
+ parser.on('-V TEXTSTYLE',
185
+ '--text-variant TEXTVARIANT',
186
+ 'Set the font variant for text overlay (e.g., "Semibold")') do |p|
187
+ options[:textvariant] = p
188
+ end
189
+
190
+ parser.on('-X TEXTXPOS',
191
+ '--text-x-position TEXTXPOS',
192
+ 'Set the X position for the text, starting from left (default is center)') do |p|
193
+ options[:xpos] = p
194
+ end
195
+
196
+ parser.on('-Y TEXTXPOS',
197
+ '--text-y-position TEXTYPOS',
198
+ 'Set the Y position for the text, starting from top (default is near bottom)') do |p|
199
+ options[:ypos] = p
200
+ end
201
+
202
+ parser.separator ''
203
+ parser.separator 'Other options:'
204
+
205
+ parser.on_tail('-v', '--verbose', 'Show ffmpeg command executed and output') do |p|
206
+ options[:verbose] = p
207
+ end
208
+
209
+ parser.on_tail('-q', '--quiet', 'Suppress all log output (overrides verbose)') do |p|
210
+ options[:quiet] = p
211
+ end
212
+
213
+ parser.on_tail('-h', '--help', 'Show this message') do
214
+ puts parser
215
+ exit
216
+ end
217
+
218
+ parser.parse!(args)
219
+ end
220
+
221
+ parser.parse!
222
+
223
+ unless is_executable?('ffmpeg')
224
+ puts 'ERROR: Requires FFmpeg to be installed!'
225
+ exit 1
226
+ end
227
+
228
+ if args.size < 1 || args.size > 2
229
+ puts 'ERROR: Specify one video to convert at a time!'
230
+ puts ''
231
+ puts parser.help
232
+ exit 1
233
+ end
234
+
235
+ unless File.exists?(args[0])
236
+ puts "ERROR: Specified video file does not exist: #{args[0]}!"
237
+ puts ''
238
+ puts parser.help
239
+ exit
240
+ end
241
+
242
+ if !is_executable?('convert') && options[:optimize]
243
+ logger.warn('ImageMagick isn\'t available! Optimization will be'\
244
+ ' disabled!') unless options[:quiet]
245
+ options[:optimize] = false
246
+ end
247
+
248
+ options
249
+ end
250
+
251
+ def self.build_filter_complex(options)
252
+ fps = options[:fps] || 15
253
+ palette_size = options[:palette] || 256
254
+ width = options[:width] # default is not to scale at all
255
+
256
+ # create filter elements
257
+ fps_filter = "fps=#{fps}"
258
+ crop_filter = options[:cropdetect] || 'crop=' + %W[
259
+ w=#{ options[:wregion] || 'in_w' }
260
+ h=#{ options[:hregion] || 'in_h' }
261
+ x=#{ options[:xoffset] || 0 }
262
+ y=#{ options[:yoffset] || 0 }
263
+ ].join(':')
264
+ scale_filter = "scale=#{width}:-1:flags=lanczos:sws_dither=none" if options[:width] unless options[:tonemap]
265
+ tonemap_filters = if options[:tonemap] # TODO: detect input format
266
+ %W[
267
+ zscale=w=#{width}:h=-1
268
+ zscale=t=linear:npl=100
269
+ format=yuv420p10le
270
+ zscale=p=bt709
271
+ tonemap=tonemap=#{options[:tonemap]}:desat=0
272
+ zscale=t=bt709:m=bt709:r=tv
273
+ format=yuv420p
274
+ ].join(',')
275
+ end
276
+ eq_filter = if options[:eq]
277
+ 'eq=' + %W[
278
+ contrast=#{options[:contrast] || 1}
279
+ brightness=#{options[:brightness] || 0}
280
+ saturation=#{options[:saturation] || 1}
281
+ gamma=#{options[:gamma] || 1}
282
+ gamma_r=#{options[:gamma_r] || 1}
283
+ gamma_g=#{options[:gamma_g] || 1}
284
+ gamma_b=#{options[:gamma_b] || 1}
285
+ ].join(':')
286
+ end
287
+ palettegen_filter = "palettegen=max_colors=#{palette_size}:stats_mode=diff"
288
+ paletteuse_filter = 'paletteuse=dither=sierra2_4a:diff_mode=rectangle'
289
+ drawtext_filter = if options[:text]
290
+ count_of_lines = options[:text].scan(/\\n/).count + 1
291
+
292
+ x = options[:xpos] || '(main_w/2-text_w/2)'
293
+ y = options[:ypos] || "(main_h-line_h*1.5*#{count_of_lines})"
294
+ size = options[:textsize] || 32
295
+ color = options[:textcolor] || 'white'
296
+ border = options[:textborder] || 3
297
+ font = options[:textfont] || 'Arial'
298
+ style = options[:textvariant] || 'Bold'
299
+ text = options[:text]
300
+ .gsub(/\\n/, ' ')
301
+ .gsub(/([:])/, '\\\\\\\\\\1')
302
+ .gsub(/([,])/, '\\\\\\1')
303
+ .gsub(/\b'\b/, "\u2019")
304
+ .gsub(/\B"\b([^"\u201C\u201D\u201E\u201F\u2033\u2036\r\n]+)\b?"\B/, "\u201C\\1\u201D")
305
+ .gsub(/\B'\b([^'\u2018\u2019\u201A\u201B\u2032\u2035\r\n]+)\b?'\B/, "\u2018\\1\u2019")
306
+
307
+ 'drawtext=' + %W[
308
+ x='#{x}'
309
+ y='#{y}'
310
+ fontsize='#{size}'
311
+ fontcolor='#{color}'
312
+ borderw='#{border}'
313
+ fontfile='#{font}'\\\\:style='#{style}'
314
+ text='#{text}'
315
+ ].join(':')
316
+ end
317
+
318
+ filter_complex = []
319
+
320
+ # first, apply the same filters we'll use later in the same order
321
+ # before applying the palettegen so that we accurately predict the
322
+ # final palette
323
+ filter_complex << fps_filter
324
+ filter_complex << crop_filter if crop_filter
325
+ filter_complex << scale_filter if options[:width] unless options[:tonemap]
326
+ filter_complex << tonemap_filters if options[:tonemap]
327
+ filter_complex << eq_filter if options[:eq]
328
+ filter_complex << drawtext_filter if options[:text]
329
+
330
+ # then generate the palette (and label this filter stream)
331
+ filter_complex << palettegen_filter + '[palette]'
332
+
333
+ # then refer back to the first video input stream and the filter
334
+ # complex stream to apply the generated palette to the video stream
335
+ # along with the other filters (drawing text last so that it isn't
336
+ # affected by scaling)
337
+ filter_complex << '[0:v][palette]' + paletteuse_filter
338
+ filter_complex << fps_filter
339
+ filter_complex << crop_filter if crop_filter
340
+ filter_complex << scale_filter if options[:width] unless options[:tonemap]
341
+ filter_complex << tonemap_filters if options[:tonemap]
342
+ filter_complex << eq_filter if options[:eq]
343
+ filter_complex << drawtext_filter if options[:text]
344
+
345
+ filter_complex.join(',')
346
+ end
347
+
348
+ def self.build_output_filename(args)
349
+ if args[1]
350
+ args[1].end_with?('.gif') ? args[1] : args[1] + '.gif'
351
+ else
352
+ File.join(File.dirname(args[0]),
353
+ File.basename(args[0], '.*') + '.gif')
354
+ end
355
+ end
356
+
357
+ def self.build_ffmpeg_gif_command(args, options, logger)
358
+ command = []
359
+ command << 'ffmpeg'
360
+ command << '-y' # always overwrite
361
+ command << '-analyzeduration' << '2147483647' << '-probesize' << '2147483647'
362
+ command << '-nostdin'
363
+ command << '-ss' << options[:seek] if options[:seek]
364
+ command << '-t' << options[:time] if options[:time]
365
+ command << '-i' << args[0]
366
+ command << '-filter_complex' << build_filter_complex(options)
367
+ command << '-f' << 'gif'
368
+
369
+ # if we're not optimizing, we won't send to stdout
370
+ command << (options[:optimize] ? '-' : build_output_filename(args))
371
+
372
+ logger.info(command.join(' ')) if options[:verbose] unless options[:quiet]
373
+
374
+ command
375
+ end
376
+
377
+ def self.build_convert_optimize_command(args, options, logger)
378
+ command = []
379
+ command << 'convert' << '-' << '-layers' << 'Optimize' << build_output_filename(args)
380
+
381
+ logger.info(command.join(' ')) if options[:verbose] unless options[:quiet]
382
+
383
+ command
384
+ end
385
+
386
+ def self.build_ffmpeg_cropdetect_command(args, options, logger)
387
+ command = []
388
+ command << 'ffmpeg'
389
+ command << '-analyzeduration' << '2147483647' << '-probesize' << '2147483647'
390
+ command << '-nostdin'
391
+ command << '-ss' << options[:seek] if options[:seek]
392
+ command << '-t' << options[:time] if options[:time]
393
+ command << '-i' << args[0]
394
+ command << '-filter_complex' << "cropdetect=limit=#{options[:cropdetect]}"
395
+ command << '-f' << 'null'
396
+ command << '-'
397
+
398
+ logger.info(command.join(' ')) if options[:verbose] unless options[:quiet]
399
+
400
+ command
401
+ end
402
+
403
+ def self.run
404
+ logger = Logger.new(STDOUT)
405
+ options = parse_args(ARGV, logger)
406
+
407
+ if options[:cropdetect]
408
+ Open3.popen3(*build_ffmpeg_cropdetect_command(ARGV, options, logger)) do |stdin, stdout, stderr, wait_thr|
409
+ stdin.close
410
+ stdout.close
411
+ stderr.each(chomp: true) do |line|
412
+ logger.info(line) if options[:verbose] unless options[:quiet]
413
+ options[:cropdetect] = line.match('crop=([0-9]+\:[0-9]+\:[0-9]+\:[0-9]+)') if line.include?('Parsed_cropdetect')
414
+ end
415
+ stderr.close
416
+
417
+ raise "Process #{wait_thr.pid} failed! Try again with --verbose to see error." unless wait_thr.value.success?
418
+ end
419
+ end
420
+
421
+ gif_pipeline_items = [build_ffmpeg_gif_command(ARGV, options, logger)]
422
+ gif_pipeline_items << build_convert_optimize_command(ARGV, options, logger) if options[:optimize]
423
+
424
+ read_io, write_io = IO.pipe
425
+ Open3.pipeline_start(*gif_pipeline_items, out: write_io, err: write_io) do |threads|
426
+ write_io.close
427
+ if options[:verbose]
428
+ read_io.each(chomp: true) { |line| logger.info(line) unless options[:quiet] }
429
+ else
430
+ read_io.read(1024) until read_io.eof?
431
+ end
432
+ read_io.close
433
+
434
+ threads.each do |t|
435
+ raise "Process #{t.pid} failed! Try again with --verbose to see error." unless t.value.success?
436
+ end
437
+ end
438
+ end
439
+ end
data/video2gif.gemspec ADDED
@@ -0,0 +1,43 @@
1
+
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'video2gif/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = 'video2gif'
8
+ spec.version = Video2gif::VERSION
9
+ spec.authors = ['Emily St.']
10
+ spec.email = ['hello@emily.st']
11
+
12
+ spec.summary = %q{Automate converting videos to GIFs using FFMpeg}
13
+ spec.homepage = 'https://github.com/emilyst/video2gif'
14
+ spec.license = 'CC0'
15
+
16
+ # Prevent pushing this gem to RubyGems.org. To allow pushes either set
17
+ # the 'allowed_push_host' to allow pushing to a single host or delete
18
+ # this section to allow pushing to any host.
19
+ if spec.respond_to?(:metadata)
20
+ spec.metadata['allowed_push_host'] = 'https://rubygems.org/'
21
+ spec.metadata['homepage_uri'] = spec.homepage
22
+ spec.metadata['source_code_uri'] = "https://github.com/emilyst/video2gif"
23
+ # spec.metadata['changelog_uri'] = "https://github.com/emilyst/video2gif"
24
+ else
25
+ raise 'RubyGems 2.0 or newer is required to protect against ' \
26
+ 'public gem pushes.'
27
+ end
28
+
29
+ # Specify which files should be added to the gem when it is released.
30
+ # The `git ls-files -z` loads the files in the RubyGem that have been
31
+ # added into git.
32
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
33
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
34
+ end
35
+ spec.bindir = 'exe'
36
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
37
+ spec.require_paths = ['lib']
38
+
39
+ spec.add_development_dependency 'bundler', '~> 2.0'
40
+ spec.add_development_dependency 'pry'
41
+ spec.add_development_dependency 'rake', '~> 10.0'
42
+ spec.add_development_dependency 'rspec', '~> 3.0'
43
+ end
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: video2gif
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Emily St.
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-04-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: pry
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description:
70
+ email:
71
+ - hello@emily.st
72
+ executables:
73
+ - video2gif
74
+ extensions: []
75
+ extra_rdoc_files: []
76
+ files:
77
+ - ".gitignore"
78
+ - ".rspec"
79
+ - ".travis.yml"
80
+ - CONTRIBUTING.md
81
+ - Gemfile
82
+ - Gemfile.lock
83
+ - LICENSE.txt
84
+ - README.md
85
+ - Rakefile
86
+ - bin/console
87
+ - bin/setup
88
+ - exe/video2gif
89
+ - lib/video2gif.rb
90
+ - lib/video2gif/version.rb
91
+ - video2gif.gemspec
92
+ homepage: https://github.com/emilyst/video2gif
93
+ licenses:
94
+ - CC0
95
+ metadata:
96
+ allowed_push_host: https://rubygems.org/
97
+ homepage_uri: https://github.com/emilyst/video2gif
98
+ source_code_uri: https://github.com/emilyst/video2gif
99
+ post_install_message:
100
+ rdoc_options: []
101
+ require_paths:
102
+ - lib
103
+ required_ruby_version: !ruby/object:Gem::Requirement
104
+ requirements:
105
+ - - ">="
106
+ - !ruby/object:Gem::Version
107
+ version: '0'
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ requirements: []
114
+ rubygems_version: 3.0.3
115
+ signing_key:
116
+ specification_version: 4
117
+ summary: Automate converting videos to GIFs using FFMpeg
118
+ test_files: []