video2gif 0.0.1

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