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 +7 -0
- data/.gitignore +11 -0
- data/.rspec +3 -0
- data/.travis.yml +7 -0
- data/CONTRIBUTING.md +26 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +41 -0
- data/LICENSE.txt +125 -0
- data/README.md +63 -0
- data/Rakefile +8 -0
- data/bin/console +11 -0
- data/bin/setup +8 -0
- data/exe/video2gif +5 -0
- data/lib/video2gif/version.rb +5 -0
- data/lib/video2gif.rb +439 -0
- data/video2gif.gemspec +43 -0
- metadata +118 -0
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
data/.rspec
ADDED
data/.travis.yml
ADDED
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
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
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
data/exe/video2gif
ADDED
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: []
|