diff --git a/.gitignore b/.gitignore
index 9b9b20a..fc3d0bc 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
+._gifcurry_trash_/*
+gifcurry-linux-*
.cabal-sandbox/*
.stack-work/*
*.stack*
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e6076c9..7f6e2f1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,54 @@
-------------------------------------------------------------------------------
+### 4.0.0.0
+
+#### Added
+
+- Multiple dynamic text overlays
+- Text overlay YAML file option `-t` to CLI
+- Text fill and outline color configuration
+- Text start and duration time configuration
+- Text origin, x translation, and y translation configuration
+- Text overlay preview to GUI
+- Text left and top placement entries to GUI
+- Text rotation configuration
+- Text outline size configuration
+- Outline and fill color selectors to GUI
+- Pattern to GUI crop preview
+- `textOverlayOriginFromString` to library API
+- `qualityFromString` to library API
+- `TextOverlays` to library API
+- `TextOverlay` to library API
+- `TextOverlayOrigin` to library API
+- `Quality` to library API
+- Text overlay validation
+- Time slices and video position display custom widget
+- Video position clock
+- Pause button for video preview
+- A complete theme
+- An icon set
+
+#### Changed
+
+- Quality percent to quality nominal
+- CLI Logo
+- CLI help information
+- GUI shows only file selection, info, and status on start up
+- GUI crop preview color
+- GUI preview size
+- GUI icon size
+- GUI first and last frame preview draw area to match the image size
+- GUI takes the video URI from the inVideoPropertiesRef instead of the inFileChooserDialog during save
+- Save as video bypasses GIF creation and goes straight to video creation
+- Video output configuration
+
+#### Removed
+
+- CLI Icon
+
+-------------------------------------------------------------------------------
+
### 3.0.0.2
#### Added
diff --git a/Gifcurry.cabal b/Gifcurry.cabal
index fa3b22c..5d1dd09 100644
--- a/Gifcurry.cabal
+++ b/Gifcurry.cabal
@@ -1,5 +1,5 @@
name: Gifcurry
-version: 3.0.0.2
+version: 4.0.0.0
synopsis: GIF creation utility.
description: Your open source video to GIF maker.
homepage: https://github.com/lettier/gifcurry
@@ -12,23 +12,86 @@ category: Application
, Library
, Graphics
build-type: Simple
-extra-source-files: README.md
- , LICENSE
- , CHANGELOG.md
+extra-source-files: ./README.md
+ , ./LICENSE
+ , ./CHANGELOG.md
+ , ./makefile
, ./lib/GtkMainSyncAsync.hs
, ./lib/GiCairoCairoBridge.hs
, ./lib/LICENSE
, ./src/dev/Paths_Gifcurry.hs
, ./src/data/style.css
+ , ./src/data/style-3-18.css
+ , ./src/data/style-3-20.css
+ , ./src/data/about-dialog-button-image.svg
+ , ./src/data/check-icon.svg
+ , ./src/data/crop-icon.svg
+ , ./src/data/down-icon.svg
+ , ./src/data/end-icon.svg
+ , ./src/data/error-icon.svg
+ , ./src/data/file-icon.svg
, ./src/data/gifcurry-logo.svg
, ./src/data/gifcurry-icon.svg
- , ./src/data/about-dialog-button-image.svg
- , ./makefile
+ , ./src/data/info-icon.svg
+ , ./src/data/left-icon.svg
+ , ./src/data/minus-icon.svg
+ , ./src/data/open-icon.svg
+ , ./src/data/plus-icon.svg
+ , ./src/data/pause-icon.svg
+ , ./src/data/pen-icon.svg
+ , ./src/data/right-icon.svg
+ , ./src/data/save-as-gif-icon.svg
+ , ./src/data/save-as-video-icon.svg
+ , ./src/data/save-icon.svg
+ , ./src/data/spiral-icon.svg
+ , ./src/data/start-icon.svg
+ , ./src/data/text-icon.svg
+ , ./src/data/up-icon.svg
+ , ./src/data/upload-icon.svg
+ , ./src/data/warning-icon.svg
+ , ./src/data/width-icon.svg
+ , ./src/data/x-icon.svg
+ , ./src/data/pattern.svg
+ , ./src/data/gray-pattern.png
+ , ./src/data/purple-pattern.png
+ , ./src/data/green-pattern.png
+ , ./src/data/orange-pattern.png
data-files: data/gui.glade
, data/style.css
+ , data/style-3-18.css
+ , data/style-3-20.css
+ , data/about-dialog-button-image.svg
+ , data/check-icon.svg
+ , data/crop-icon.svg
+ , data/down-icon.svg
+ , data/end-icon.svg
+ , data/error-icon.svg
+ , data/file-icon.svg
, data/gifcurry-logo.svg
, data/gifcurry-icon.svg
- , data/about-dialog-button-image.svg
+ , data/info-icon.svg
+ , data/left-icon.svg
+ , data/minus-icon.svg
+ , data/open-icon.svg
+ , data/plus-icon.svg
+ , data/pause-icon.svg
+ , data/pen-icon.svg
+ , data/right-icon.svg
+ , data/save-as-gif-icon.svg
+ , data/save-as-video-icon.svg
+ , data/save-icon.svg
+ , data/spiral-icon.svg
+ , data/start-icon.svg
+ , data/text-icon.svg
+ , data/up-icon.svg
+ , data/upload-icon.svg
+ , data/warning-icon.svg
+ , data/width-icon.svg
+ , data/x-icon.svg
+ , data/gray-pattern.png
+ , data/purple-pattern.png
+ , data/green-pattern.png
+ , data/orange-pattern.png
data-dir: ./src/
cabal-version: >= 1.10
@@ -37,13 +100,14 @@ source-repository head
location: https://github.com/lettier/gifcurry
library
- exposed-modules: Gifcurry
+ exposed-modules: Gifcurry
build-depends: base >= 4.7 && < 5
, process >= 1.2 && <= 1.4.4
, temporary >= 1.2 && < 1.3
, directory == 1.3.*
, text == 1.2.*
, filepath == 1.4.*
+ , filemanip == 0.3.6.*
hs-source-dirs: ./src
, ./src/lib/
ghc-options: -Wall -freverse-errors
@@ -56,24 +120,29 @@ executable gifcurry_gui
, haskell-gi-base == 0.21.*
, gi-gobject == 2.0.*
, gi-glib == 2.0.*
+ , gi-pango == 1.0.*
, gi-gdk == 3.0.*
+ , gi-gdkpixbuf == 2.0.15
, gi-gtk == 3.0.*
, gi-cairo == 1.0.*
, gi-gst == 1.0.*
, gi-gstvideo == 1.0.*
, cairo == 0.13.*
+ , pango == 0.13.*
, bytestring == 0.10.*
, process >= 1.2 && <= 1.4.4
, temporary >= 1.2 && < 1.3
, directory == 1.3.*
, text == 1.2.*
, filepath == 1.4.*
+ , filemanip == 0.3.6.*
, transformers == 0.5.*
other-modules: Paths_Gifcurry
, GuiRecords
, GuiCapabilities
, Gifcurry
, GuiStyle
+ , GuiTextOverlays
, GuiPreview
, GuiMisc
@@ -95,6 +164,10 @@ executable gifcurry_cli
, cmdargs == 0.10.*
, text == 1.2.*
, filepath == 1.4.*
+ , filemanip == 0.3.6.*
+ , aeson == 1.1.2.*
+ , bytestring == 0.10.8.*
+ , yaml == 0.8.23.*
other-modules: Gifcurry
ghc-options: -Wall -freverse-errors
hs-source-dirs: ./src/
diff --git a/README.md b/README.md
index 146cf23..2bad952 100644
--- a/README.md
+++ b/README.md
@@ -1,8 +1,8 @@
-![Gifcurry](https://i.imgur.com/1omeH3m.png)
+![Gifcurry](https://i.imgur.com/9pS8Ibp.png)
# Tell me about Gifcurry.
-Gifcurry is your only open source video to GIF maker built with Haskell.
+Gifcurry is your only open source video-to-GIF maker built with Haskell.
Load a video, make some edits, and save it as a GIF—it's that easy.
Most video formats should work so go wild.
And since it's made with Haskell, you know it's good.
@@ -13,104 +13,127 @@ And for the Haskell programmers out there, there is also a library API.
Gifcurry can save your creation as a GIF or as a video.
So if you hate GIFs with a passion—no problem!
-Just select "save as video" and do your part to rid the world of GIFs.
+Just select "Save as a Video" and do your part to rid the world of GIFs.
-Enjoy memes? Great! Gifcurry can add text to the top and/or the bottom of your GIF.
-Just type in some text for the top or type in some text for the bottom or type in
-some pithy text for both the top and bottom—Gifcurry don't care.
-Oh and you can select the font too so you're never too far from Comic Sans.
+Enjoy memes? Great! Gifcurry can add text all over your GIF.
+You can change the font, size, color, position, outline, rotation, and the timing.
+Create the next viral meme with Gifcurry.
-Gifcurry caters to the power user with its crop tool.
+Did you know Gifcurry slices...and dices?
You can crop from the left, the right, the top, and/or the bottom.
-With Gifcurry, you can cut out anything you don't want.
+With Gifcurry, you can slice up some tasty GIFs.
-Is Gifcurry another Electron app? No way! Gifcurry is 100% #electronfree.
+Is Gifcurry another Electron app? No way! Gifcurry is 100% #ElectronFree.
No need to download more RAM, Gifcurry is light as a feather.
Run it all day, run it all year—you'll never notice.
-"So...Gifcurry is just FFmpeg and ImageMagick." Nope.
+"So...Gifcurry is just FFmpeg and ImageMagick?"—nope.
Gifcurry hides all the goofy details so you can concentrate on what matters—the almighty GIF.
Making GIFs with Gifcurry is fun so try it out!
## What do I need Gifcurry for?
-Need to show off that new UI feature in a pull request? Gifcurry.
+Want to show off that new UI feature in a pull request? Gifcurry.
Your template doesn't allow video in the hero image? Gifcurry.
No GIF of your favorite movie scene? Gifcurry.
Need a custom animated emoji for Slack? Gifcurry.
-Have an idea of the perfect GIF to close out that email? Gifcurry.
+Can't find the perfect GIF for that reply-all email? Gifcurry.
Your README needs a GIF? Gifcurry.
+That presentation slide could use some animation? Gifcurry.
Video doesn't auto play on iOS? Gifcurry.
Gifcurry comes in handy for all sorts of scenarios.
## What does the GUI look like?
-![Gifcurry GUI](https://i.imgur.com/dVpQfHq.gif)
+![Gifcurry GUI](https://i.imgur.com/IhB50O1.gif)
-## Got any sample GIFs?
-
-![GIF](https://i.imgur.com/alxcMli.gif)
-![GIF](https://i.imgur.com/FUjIBm2.gif)
## How do I use the command line interface (CLI)?
-```bash
+```text
gifcurry_cli [OPTIONS]
FILE IO:
- -i --input-file=FILE The input video file path and name.
- -o --output-file=FILE The output GIF file path and name.
- -m --save-as-video If present, saves the GIF as a video.
+ -i --input-file=FILE The input video file path and name.
+ -o --output-file=FILE The output GIF file path and name.
+ -m --save-as-video If present, saves the GIF as a video.
TIME:
- -s --start-time=NUM The start time (in seconds) for the first frame.
- -d --duration-time=NUM How long the GIF lasts (in seconds) from the
- start time.
+ -s --start-time=NUM The start time (in seconds) for the first
+ frame.
+ -d --duration-time=NUM How long the GIF lasts (in seconds) from the
+ start time.
OUTPUT FILE SIZE:
- -w --width-size=INT How wide the GIF needs to be. Height will scale
- to match.
- -q --quality-percent=NUM From 1 (very low quality) to 100 (the best
- quality). Controls how many colors are used and how
- many frames per second there are.
-TEXT:
- -f --font-choice=TEXT Choose your desired font for the top and bottom
- text.
- -t --top-text=TEXT The text you wish to add to the top of the GIF.
- -b --bottom-text=TEXT The text you wish to add to the bottom of the
- GIF.
+ -w --width-size=INT How wide the GIF needs to be. Height will
+ scale to match.
+ -q --quality=ITEM Controls how many colors are used and the
+ frame rate.
+ The options are High, Medium, and Low.
CROP:
- -L --left-crop=NUM The amount you wish to crop from the left.
- -R --right-crop=NUM The amount you wish to crop from the right.
- -T --top-crop=NUM The amount you wish to crop from the top.
- -B --bottom-crop=NUM The amount you wish to crop from the bottom.
-INFO:
- -? --help Display help message
- -V --version Print version information
+ -L --left-crop=NUM The amount you wish to crop from the left.
+ -R --right-crop=NUM The amount you wish to crop from the right.
+ -T --top-crop=NUM The amount you wish to crop from the top.
+ -B --bottom-crop=NUM The amount you wish to crop from the bottom.
+TEXT:
+ -t --text-overlays-file=FILE The text overlays YAML file path and name.
+ The format is:
+ - text: ...
+ fontFamily: ...
+ fontStyle: ...
+ fontStretch: ...
+ fontWeight: ...
+ fontSize: ...
+ origin: ...
+ xTranslation: ...
+ yTranslation: ...
+ rotation: ...
+ startTime: ...
+ durationTime: ...
+ outlineSize: ...
+ outlineColor: ...
+ fillColor: ...
+ - text: ...
+ ...
+
+
+ -? --help Display help message
+ -V --version Print version information
+
+Visit https://github.com/lettier/gifcurry for more information.
```
## Got a CLI example?
```text
gifcurry_cli \
--i ~/Videos/video.webm -o ~/tmp/test -m \
--L 25 -R 25 -T 25 -B 25 \
--s 149.11 -d 1 \
--f 'fira sans' -t 'Top Text' -b 'Bottom Text'
-
- ppDPPPDbDDpp
- pDPPPP )DPDp )
- PPPPP )pp DPPp ppppp PPP pDbDD
- p )PPP PPPD PPPD pDPDPPPDP PPP
- bP DPP pPPP )PPPb (PPP PPP )PPPPPP pDPPPDb PPP PPb PPbpDPP PPbpPP ·DPb pPD
- (PPb )D (PPD bPPP PPP DDDDD PPP PPP PPb PPP PPb PPPP PPPP (PP pPPC
- (PPPp PPP b )PPP DPPp PPP PPP PPP (PPb PPP PPb PPP PPP DPb PPP
- PPPb DPPP pPp DPb DPDp PPP PPP PPP DPPp p PPP pPPb PPP PPP PPpPP
- )PPPp (DPPP )PPb b (PPDDPPP PPP PPP (PDDDPC PDDP PPC PPP PPP )DPPP
- )DPPp )DD DPPPb pbPP
- )DPbp (PPPPPb PPC
- SPDbDppppPPDPC
-
-Gifcurry 3.0.0.2
+ -i ~/Videos/video.webm \
+ -o ~/tmp/test \
+ -s 150 \
+ -d 1 \
+ -t ~/tmp/text-overlays.yaml \
+ -w 800 \
+ -q High \
+ -L 0.1 \
+ -R 0.1 \
+ -T 0.1 \
+ -B 0.1 \
+ -m
+
+ ▄▄▄▄▄▄▄▄
+ ▄▄████ ▀▀███▄
+ ████▀ ▄ ▀███ ▄ ▐██▌ ▄███▄
+ ▄ ▐███ ████ ▀███ ▄███▀▀██ ███
+ ▐█▌ ██ ▐███ ████ ███ ▐██ █████▌ ▄█████ ▐██▌ ██▌ ██▄██▌ ██▄██▌ ██▌ ███
+ ███ ▐▌ ███ ▐███▌ ███ ████▌ ▐██ ██▌ ███ ▐██▌ ██▌ ███▀ ███▀ ▐██ ███
+ ████ ███▀ ▐█ ███▌ ███ ██▌ ▐██ ██▌ ███ ▐██▌ ██▌ ██▌ ██▌ ██▌▐██
+ ▐███▄ ▐██▌ ██ ██ ███▄▄▄██▌ ▐██ ██▌ ███▄▄█ ███▄███▌ ██▌ ██▌ ████▌
+ ▀███ ▀███ ▐███ ▀ ▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀ ███
+ ███▄ ▀ ████▌ ███▀
+ ▀███▄▄ █████▀
+ ▀▀▀▀▀▀▀
+
+
+Gifcurry 4.0.0.0
(C) 2016 David Lettier
lettier.com
@@ -121,24 +144,41 @@ lettier.com
- Output File: /home/tmp/test.webm
- Save As Video: Yes
- TIME:
- - Start Second: 149.110
+ - Start Second: 150.000
- Duration Time: 1.000 seconds
- OUTPUT FILE SIZE:
- - Width Size: 500px
- - Quality Percent: 100.0%
+ - Width Size: 800px
+ - Quality: High
- TEXT:
- - Font Choice: fira sans
- - Top Text: Top Text
- - Bottom Text: Bottom Text
+ - Text: This is a test.
+ - Font:
+ - Family: Sans
+ - Size: 30
+ - Style: Normal
+ - Stretch: Normal
+ - Weight: 800
+ - Time:
+ - Start: 150.000 seconds
+ - Duration: 20.000 seconds
+ - Translation:
+ - Origin: NorthWest
+ - X: 0.0
+ - Y: 0.0
+ - Rotation:
+ - Degrees: 0
+ - Outline:
+ - Size: 10
+ - Color: rgb(1,100,10)
+ - Fill:
+ - Color: rgb(255,255,0)
- CROP:
- - Left Crop: 25.000
- - Right crop: 25.000
- - Top Crop: 25.000
- - Bottom Crop: 25.000
-
-[INFO] Writing the temporary frames to: /home/.cache/gifcurry/gifcurry-frames17389
-[INFO] Your font choice matched to "Fira-Sans".
-[INFO] Saving your GIF to: /home/.cache/gifcurry/gifcurry-frames17389/finished-result.gif
+ - Left: 0.100
+ - Right: 0.100
+ - Top: 0.100
+ - Bottom: 0.100
+
+[INFO] Writing the temporary frames to: /home/.cache/gifcurry/gifcurry-frames30450
+[INFO] Adding text...
[INFO] Saving your video to: /home/tmp/test.webm
[INFO] All done.
```
@@ -153,15 +193,15 @@ To find the latest version of Gifcurry, head over to the
### I use Linux.
If you use Linux then the easiest way to grab a copy of Gifcurry is by downloading the
-[AppImage](https://github.com/lettier/gifcurry/releases/download/3.0.0.2/gifcurry-3.0.0.2-x86_64.AppImage).
+[AppImage](https://github.com/lettier/gifcurry/releases/download/4.0.0.0/gifcurry-4.0.0.0-x86_64.AppImage).
After you download the
-[AppImage](https://github.com/lettier/gifcurry/releases/download/3.0.0.2/gifcurry-3.0.0.2-x86_64.AppImage),
+[AppImage](https://github.com/lettier/gifcurry/releases/download/4.0.0.0/gifcurry-4.0.0.0-x86_64.AppImage),
right click on it, select permissions, and check the box near execute.
With that out of the way—you're all set—just double click on the AppImage
and the GUI will fire right up.
You can also download and install the
-[AppImage](https://github.com/lettier/gifcurry/releases/download/3.0.0.2/gifcurry-3.0.0.2-x86_64.AppImage)
+[AppImage](https://github.com/lettier/gifcurry/releases/download/4.0.0.0/gifcurry-4.0.0.0-x86_64.AppImage)
using the handy
[AppImage install script](https://raw.githubusercontent.com/lettier/gifcurry/master/packaging/linux/app-image/gifcurry-app-image-install.sh)
(right click and save link as).
@@ -169,7 +209,7 @@ Download the script, right click on it, select permissions, check the box near e
You should now see Gifcurry listed alongside your other installed programs.
If you want the CLI then download the
-[prebuilt version](https://github.com/lettier/gifcurry/releases/download/3.0.0.2/gifcurry-linux-3.0.0.2.tar.gz)
+[prebuilt version](https://github.com/lettier/gifcurry/releases/download/4.0.0.0/gifcurry-linux-4.0.0.0.tar.gz)
for Linux, extract it, open up your terminal,
`cd` to the bin folder, and then run `gifcurry_cli -?`.
As an added bonus, inside the bin directory is the GUI version
@@ -215,7 +255,7 @@ The
[Gifcurry snap](https://snapcraft.io/gifcurry)
only comes with the GUI.
If you want the CLI, download the
-[prebuilt version](https://github.com/lettier/gifcurry/releases/download/3.0.0.2/gifcurry-linux-3.0.0.2.tar.gz)
+[prebuilt version](https://github.com/lettier/gifcurry/releases/download/4.0.0.0/gifcurry-linux-4.0.0.0.tar.gz)
for Linux.
### I use Mac.
@@ -250,6 +290,7 @@ $HOME/.local/bin/gifcurry_gui
* [GTK+ >= 3.10](http://www.gtk.org/download/index.php)
* [FFmpeg >= 3](https://www.ffmpeg.org/download.html)
* [GStreamer >= 1.0](https://gstreamer.freedesktop.org/download/)
+ * [GStreamer Plugins](https://gstreamer.freedesktop.org/modules/)
* [ImageMagick >= 6](http://www.imagemagick.org/script/download.php)
### To build Gifcurry.
diff --git a/docs/gifcurry-ui-0.gif b/docs/gifcurry-ui-0.gif
index 84387f8..76652c8 100644
Binary files a/docs/gifcurry-ui-0.gif and b/docs/gifcurry-ui-0.gif differ
diff --git a/docs/gifcurry-ui-1.gif b/docs/gifcurry-ui-1.gif
index 558dc9d..6043c59 100644
Binary files a/docs/gifcurry-ui-1.gif and b/docs/gifcurry-ui-1.gif differ
diff --git a/docs/gifcurry-ui-2.gif b/docs/gifcurry-ui-2.gif
index 094e725..bda7531 100644
Binary files a/docs/gifcurry-ui-2.gif and b/docs/gifcurry-ui-2.gif differ
diff --git a/docs/gifcurry-ui-3.gif b/docs/gifcurry-ui-3.gif
index 5824a74..cb5d2ac 100644
Binary files a/docs/gifcurry-ui-3.gif and b/docs/gifcurry-ui-3.gif differ
diff --git a/docs/index.html b/docs/index.html
index b4be43d..9ac1773 100644
--- a/docs/index.html
+++ b/docs/index.html
@@ -125,8 +125,8 @@
Linux users can download the
- AppImage or
- the prebuilt binaries .
+ AppImage or
+ the prebuilt binaries .
If you'd rather install it, you can do so via
pacman (Arch)
or
diff --git a/docs/screenshot.jpg b/docs/screenshot.jpg
index 4a85294..381f131 100644
Binary files a/docs/screenshot.jpg and b/docs/screenshot.jpg differ
diff --git a/icon/icon-1-0.svg b/icon/icon-1-0.svg
index 075f0bf..00125d5 100644
--- a/icon/icon-1-0.svg
+++ b/icon/icon-1-0.svg
@@ -1,6 +1,4 @@
-
-
+ height="360"
+ width="360"
+ version="1.1"
+ id="svg4247">
@@ -34,325 +25,203 @@
-
-
-
-
-
-
+ d="m 568.74979,47.923777 131.4827,201.742193 -56.4097,104.44198 -65.9398,-80.1473"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ d="m 620.30939,138.88105 -63.8266,110.33378 -93.4282,-127.76124 80.6828,-108.222263 17.6284,26.48706"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ d="M 567.63459,46.250997 699.78419,249.65402"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 620.60599,138.81143 -64.1232,110.84259"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ id="path4423" />
+ id="path4425" />
+ id="path4427" />
+ id="path4429" />
+ id="path4439" />
+ id="path4441" />
+ id="path4443" />
+ id="path4445" />
+ d="m 578.22889,47.923777 131.4827,201.742193 -56.4097,104.44198 -65.9398,-80.1473"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ d="m 629.78839,138.88105 -63.8265,110.33378 -93.4283,-127.76124 80.6829,-108.222263 17.6284,26.48706"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="M 577.11369,46.250997 709.26319,249.65402"
+ style="fill:none;fill-rule:evenodd;stroke:#a427c0;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 630.08509,138.81143 -64.1232,110.84259"
+ style="fill:none;fill-rule:evenodd;stroke:#a427c0;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 588.82319,47.923777 131.4826,201.742193 -56.4097,104.44198 -65.9397,-80.1473"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ d="m 640.38269,138.88105 -63.8266,110.33378 -93.4282,-127.76124 80.6829,-108.222263 17.6284,26.48706"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ d="M 587.70799,46.250997 719.85749,249.65402"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 640.67929,138.81143 -64.1232,110.84259"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ id="path4463" />
+ id="path4465" />
+ id="path4467" />
+ id="path4469" />
+ preserveAspectRatio="none"
+ xlink:href="data:image/png;base64,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 8LYAAAAASUVORK5CYII= "
+ id="image4173"
+ x="987.93323"
+ y="-90.259583" />
+ d="m 862.07259,73.6407 117.6521,176.19937"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 860.79409,250.79194 57.432,-85.86931"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 860.96839,250.36722 -80.5542,-118.985 61.3565,-89.602073 12.4395,19.804663"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 881.24659,280.79116 39.795,55.55914"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ id="path4198" />
+ style="display:inline"
+ id="layer1">
+ d="m 142.48086,276.88801 39.795,55.55914"
+ id="path4242" />
+ d="m 181.96976,332.83382 58.9867,-88.19368"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.02706873px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ d="m 122.02826,246.88879 57.4321,-85.86931"
+ style="fill:none;fill-rule:evenodd;stroke:#ea2a89;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 122.20266,246.46407 -80.5543,-118.985 61.3566,-89.602074 12.02058,19.213246"
+ style="fill:none;fill-rule:evenodd;stroke:#ea2a89;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 142.48086,276.88801 39.64195,55.75247 58.83485,-87.35195 -117.6509,-175.550981"
+ style="fill:none;fill-rule:evenodd;stroke:#ea2a89;stroke-width:16;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 146.07926,246.88879 56.9392,-85.08075"
+ style="fill:none;fill-rule:evenodd;stroke:#5cb86b;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 166.92604,276.88801 39.64195,55.75247 58.8349,-87.35195 L 147.75204,69.737549"
+ style="fill:none;fill-rule:evenodd;stroke:#5cb86b;stroke-width:18;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 173.67866,246.88879 56.2492,-84.19363"
+ id="path4288" />
+ d="m 194.13126,276.88801 39.6419,55.75247 58.8349,-87.35195 -117.6509,-175.550981"
+ id="path4292" />
(C) DAVID LETTIER
+ x="-140.73804"
+ y="400.33884">(C) DAVID LETTIER
+ style="display:inline"
+ id="layer2">
+ d="M 122.31861,245.69345 182.53865,157.03618 103.08166,37.71129 42.025227,127.20496 Z"
+ style="fill:#ea2a89;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ id="path4210" />
+ d="M 173.96899,245.69345 234.18903,157.03618 154.73204,37.71129 93.675611,127.20496 Z"
+ style="fill:#3f9ce6;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ d="m 146.25356,246.46407 -80.5542,-118.985 61.3565,-89.602074 12.4395,19.804663"
+ style="display:inline;fill:none;fill-rule:evenodd;stroke:#5cb86b;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ id="path4290" />
diff --git a/icon/icon-1-1.svg b/icon/icon-1-1.svg
index 8edb9c3..d447b00 100644
--- a/icon/icon-1-1.svg
+++ b/icon/icon-1-1.svg
@@ -1,24 +1,15 @@
-
-
+ height="328.05896"
+ width="328.05896"
+ version="1.1"
+ id="svg4247">
@@ -33,152 +24,75 @@
-
-
-
-
-
-
+ transform="translate(12.507442,-35.374773)"
+ id="g4549">
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ id="path4244" />
+ id="path4246" />
+ id="path4248" />
+ id="path4250" />
+ d="m 115.7164,280.79116 39.64195,55.75247 58.83365,-88.00034"
+ id="path4252" />
+ id="path4266" />
+ id="path4268" />
+ id="path4270" />
+ d="m 139.7673,280.79116 39.64195,55.75247 58.83365,-88.00034"
+ id="path4272" />
+ d="M 148.1927,73.6407 265.8448,249.84007"
+ style="fill:none;fill-rule:evenodd;stroke:#3f9ce6;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 146.9142,250.79194 56.2492,-84.19363"
+ style="fill:none;fill-rule:evenodd;stroke:#3f9ce6;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="M 147.0886,250.36722 66.5343,131.38222 127.8909,41.780147 140.3304,61.58481"
+ style="fill:none;fill-rule:evenodd;stroke:#3f3f3f;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ style="fill:none;fill-rule:evenodd;stroke:#3f3f3f;stroke-width:6;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
(C) DAVID LETTIER
+ id="tspan4504"
+ style="font-size:7.5px;line-height:1.25">(C) DAVID LETTIER
diff --git a/icon/icon-2.svg b/icon/icon-2.svg
index 5b1cbde..c1410fc 100644
--- a/icon/icon-2.svg
+++ b/icon/icon-2.svg
@@ -6,36 +6,11 @@
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="57mm"
- height="57mm"
- viewBox="0 0 57.000001 57"
- version="1.1"
id="svg8"
- sodipodi:docname="icon-3.svg"
- inkscape:version="0.92.1 r">
-
+ version="1.1"
+ viewBox="0 0 57.000001 57"
+ height="57mm"
+ width="57mm">
+ transform="translate(-2.857145,-2.6012127)">
+ d="M 30.00902,4.9971347 A 25.513393,25.513393 0 0 0 14.616162,10.591115 c 0.133368,0.18103 0.263304,0.36156 0.393258,0.54208 A 25.513393,25.513393 0 0 1 30.693217,5.0178047 25.513393,25.513393 0 0 0 30.00902,4.9971047 Z M 11.255127,13.816235 a 25.513393,25.513393 0 0 0 -6.250264,17.18603 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 0.382921,-0.0207 25.513393,25.513393 0 0 1 -25.321967,-24.99124 25.513393,25.513393 0 0 1 5.66632,-16.52147 c -0.155829,-0.22395 -0.313022,-0.44708 -0.474389,-0.66456 z"
+ style="display:inline;opacity:1;fill:#ff29ac;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ d="m 32.584056,4.9971347 a 25.513393,25.513393 0 0 0 -0.433565,0.0238 25.513393,25.513393 0 0 1 25.219134,24.9726403 l 5.3e-4,0.031 a 25.513393,25.513393 0 0 1 -5.50561,16.28325 c 0.1834,0.26966 0.3692,0.53514 0.56018,0.79478 a 25.513393,25.513393 0 0 0 6.17275,-17.07803 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 32.584056,4.9971347 Z M 48.613575,49.702415 a 25.513393,25.513393 0 0 1 -15.819712,6.28747 25.513393,25.513393 0 0 0 0.783415,0.0243 25.513393,25.513393 0 0 0 15.502927,-5.66839 c -0.1577,-0.21469 -0.31368,-0.42907 -0.46663,-0.64337 z"
+ style="display:inline;opacity:1;fill:#29ffa4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ d="M 31.59652,4.9971347 A 25.513393,25.513393 0 0 0 15.533936,11.126475 c 3.454616,4.79733 5.736982,9.58836 8.496639,23.93859 l 7.284826,-24.57937 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534349,4.71687 12.525849,13.20694 3.15251,13.38741 5.09705,18.65842 7.79436,22.62188 a 25.513393,25.513393 0 0 0 5.50095,-16.28997 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 31.59652,4.9971347 Z M 12.254549,14.474595 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 16.268773,-6.304 c -3.38483,-4.74042 -5.64381,-9.58763 -8.370031,-23.76393 l -7.28431,24.57938 -0.451652,-0.11162 -0.02222,0.11162 c -7.861171,-1.46742 -10.534351,-4.71687 -12.52585,-13.20695 -3.212296,-13.64125 -5.171564,-18.85363 -7.949903,-22.84408 z m 22.52679,0.85214 -7.606255,23.40684 H 25.02638 c 0.8393,3.5098 2.013317,5.65789 4.711855,6.95152 l 7.605739,-23.40736 h 2.149221 c -0.839229,-3.50953 -2.013754,-5.65733 -4.711856,-6.951 z"
+ id="path4679" />
+ d="M 31.350966,4.9971347 A 25.513393,25.513393 0 0 0 15.288382,11.126475 c 3.454616,4.79733 5.736982,9.58836 8.496639,23.93859 l 7.284826,-24.57937 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534343,4.71687 12.525843,13.20694 3.15251,13.38741 5.09705,18.65842 7.79436,22.62188 a 25.513393,25.513393 0 0 0 5.50095,-16.28997 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 31.350966,4.9971347 Z M 12.008995,14.474595 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 16.268767,-6.304 c -3.38483,-4.74042 -5.64381,-9.58763 -8.370025,-23.76393 l -7.28431,24.57938 -0.451652,-0.11162 -0.02222,0.11162 c -7.861171,-1.46742 -10.534351,-4.71687 -12.52585,-13.20695 -3.212296,-13.64125 -5.171564,-18.85363 -7.949903,-22.84408 z m 22.52679,0.85214 -7.606255,23.40684 h -2.148704 c 0.8393,3.5098 2.013317,5.65789 4.711855,6.95152 l 7.605739,-23.40736 h 2.149221 c -0.839229,-3.50953 -2.013754,-5.65733 -4.711856,-6.951 z"
+ style="display:inline;opacity:1;fill:#1b59a2;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ d="M 31.067353,4.9971347 A 25.513393,25.513393 0 0 0 15.004769,11.126475 c 3.454616,4.79733 5.736982,9.58836 8.496639,23.93859 l 7.284826,-24.57937 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534346,4.71687 12.525846,13.20694 3.15251,13.38741 5.09705,18.65842 7.79436,22.62188 a 25.513393,25.513393 0 0 0 5.50095,-16.28997 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 31.067353,4.9971347 Z M 11.725382,14.474595 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 16.26877,-6.304 c -3.38483,-4.74042 -5.64381,-9.58763 -8.370028,-23.76393 l -7.28431,24.57938 -0.451652,-0.11162 -0.02222,0.11162 c -7.861171,-1.46742 -10.534351,-4.71687 -12.52585,-13.20695 -3.212296,-13.64125 -5.171564,-18.85363 -7.949903,-22.84408 z m 22.52679,0.85214 -7.606255,23.40684 h -2.148704 c 0.8393,3.5098 2.013317,5.65789 4.711855,6.95152 l 7.605739,-23.40736 h 2.149221 c -0.839229,-3.50953 -2.013754,-5.65733 -4.711856,-6.951 z"
+ style="display:inline;opacity:1;fill:#5294e2;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ transform="translate(-3.1131187,-1.2102795)">
+ Gifcurr
+ λ
Gifcurr
λ
- Gifcurr
λ
- λ
+ id="tspan6123">λ
Gifcurr
+ y="38.733902"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#444a58;fill-opacity:1;stroke-width:0.26458332px">Gifcurr
+ id="g4644"
+ transform="translate(-2.857145,-2.6012127)">
+ d="M 23.810702,55.129827 C 17.778485,53.503104 9.0148866,48.566654 5.6981798,36.315722"
+ style="opacity:1;vector-effect:none;fill:none;fill-opacity:1;fill-rule:evenodd;stroke:#000000;stroke-width:1;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1107.35998535;stroke-opacity:0" />
© 2018 David Lettier
+ dy="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0">© 2018 David Lettier
diff --git a/icon/icon-3.svg b/icon/icon-3.svg
index 7e72a67..1decc73 100644
--- a/icon/icon-3.svg
+++ b/icon/icon-3.svg
@@ -5,34 +5,34 @@
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
- width="55.240219mm"
- height="55.272121mm"
- viewBox="0 0 55.240221 55.272122"
+ id="svg8"
version="1.1"
- id="svg8">
+ viewBox="0 0 55.240221 55.272122"
+ height="55.272121mm"
+ width="55.240219mm">
+ width="1.0769389"
+ x="-0.038469434"
+ id="filter1015"
+ style="color-interpolation-filters:sRGB">
+ id="feGaussianBlur1017"
+ stdDeviation="2.0172845" />
+ width="1.1200387"
+ x="-0.060019407"
+ id="filter1023"
+ style="color-interpolation-filters:sRGB">
+ id="feGaussianBlur1025"
+ stdDeviation="1.2333967" />
+ id="g1094">
+ style="display:inline;opacity:1;fill:#13082a;fill-opacity:0.60784314;fill-rule:nonzero;stroke:none;stroke-width:0.58234793;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;filter:url(#filter1023)" />
+ transform="translate(-125.34941,72.964581)">
+ id="path884" />
+ id="path878" />
+ id="path886" />
+ id="path888" />
+ id="path890" />
+ id="path892" />
+ id="path894" />
+ id="path851" />
diff --git a/logo/logo-1.svg b/logo/logo-1.svg
index e0b2d66..d568aea 100644
--- a/logo/logo-1.svg
+++ b/logo/logo-1.svg
@@ -1,24 +1,15 @@
-
-
+ height="384"
+ width="384"
+ version="1.1"
+ id="svg4247">
@@ -33,198 +24,120 @@
-
-
-
-
-
-
+ d="m 125.90653,283.63875 39.795,55.55914"
+ id="path4242" />
+ d="m 165.39543,339.58456 58.9867,-88.19368"
+ style="fill:none;fill-rule:evenodd;stroke:#000000;stroke-width:1.02706873px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1" />
+ d="M 106.73243,76.488293 224.38453,252.68766"
+ style="fill:none;fill-rule:evenodd;stroke:#ea2a89;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 105.45393,253.63953 57.4321,-85.86931"
+ style="fill:none;fill-rule:evenodd;stroke:#ea2a89;stroke-width:2;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 105.62833,253.21481 -80.554303,-118.985 61.3566,-89.60207 12.02058,19.213246"
+ style="fill:none;fill-rule:evenodd;stroke:#3f3f3f;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 125.90653,283.63875 39.64195,55.75247 58.83365,-88.00034"
+ style="fill:none;fill-rule:evenodd;stroke:#3f3f3f;stroke-width:2;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="M 130.78343,76.488293 248.43553,252.68766"
+ style="fill:none;fill-rule:evenodd;stroke:#5cb86b;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 129.50493,253.63953 56.9392,-85.08075"
+ style="fill:none;fill-rule:evenodd;stroke:#5cb86b;stroke-width:4;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 129.67923,253.21481 -80.554203,-118.985 61.356503,-89.60207 12.4395,19.804663"
+ style="fill:none;fill-rule:evenodd;stroke:#3f3f3f;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ d="m 149.95743,283.63875 39.64195,55.75247 58.83365,-88.00034"
+ style="fill:none;fill-rule:evenodd;stroke:#3f3f3f;stroke-width:4;stroke-linecap:round;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
+ id="path4286" />
+ d="m 157.10433,253.63953 56.2492,-84.19363"
+ id="path4288" />
+ id="path4290" />
+ d="m 177.55693,283.63875 39.6419,55.75247 58.8337,-88.00034"
+ id="path4292" />
+ transform="rotate(0.19415858)"
+ aria-label="Gifcurry">
+ d="m 257.95808,163.97894 q -0.35156,0.46875 -0.95703,1.15235 -0.58593,0.66406 -1.58203,1.28906 -0.99609,0.60547 -2.46094,1.05469 -1.44531,0.42968 -3.51562,0.42968 -2.20703,0 -4.04297,-0.74218 -1.81641,-0.74219 -3.125,-2.22657 -1.30859,-1.48437 -2.03125,-3.73046 -0.72266,-2.2461 -0.72266,-5.23438 v -5.35156 q 0,-11.9336 9.55079,-11.9336 2.07031,0 3.67187,0.60547 1.62109,0.58594 2.73438,1.71875 1.11328,1.11328 1.73828,2.73438 0.625,1.62109 0.74218,3.67187 h -3.57421 q -0.21485,-2.65625 -1.48438,-4.14062 -1.25,-1.50391 -3.80859,-1.50391 -1.64063,0 -2.77344,0.54688 -1.13281,0.54687 -1.85547,1.66015 -0.70312,1.09375 -1.03516,2.75391 -0.3125,1.64062 -0.3125,3.84766 v 5.39062 q 0,2.26563 0.44922,3.94531 0.44922,1.67969 1.26953,2.77344 0.83985,1.09375 2.01172,1.64063 1.17188,0.52734 2.61719,0.52734 1.23047,0 2.07031,-0.21484 0.83985,-0.21485 1.38672,-0.52735 0.56641,-0.3125 0.89844,-0.625 0.33203,-0.3125 0.56641,-0.52734 v -6.5625 h -5.19532 v -3.04688 h 8.76953 z" />
+ d="m 267.07918,167.5141 h -3.45703 v -21.13281 h 3.45703 z m -3.65235,-26.73828 q 0,-0.87891 0.48829,-1.48438 0.48828,-0.60547 1.46484,-0.60547 0.97656,0 1.46484,0.625 0.50782,0.60547 0.50782,1.46485 0,0.8789 -0.48829,1.46484 -0.48828,0.58594 -1.48437,0.58594 -0.97656,0 -1.46484,-0.58594 -0.48829,-0.58594 -0.48829,-1.46484 z" />
+ d="m 274.01277,167.5141 v -18.33984 h -2.7539 v -2.79297 h 2.7539 v -2.22657 q 0,-1.69921 0.41016,-3.00781 0.42969,-1.32812 1.21094,-2.22656 0.78125,-0.89844 1.91406,-1.34766 1.13281,-0.46875 2.53906,-0.46875 1.03516,0 2.08984,0.33203 l -0.17578,2.92969 q -0.33203,-0.0781 -0.76172,-0.11719 -0.42968,-0.0586 -0.82031,-0.0586 -1.40625,0 -2.1875,1.03516 -0.76172,1.01562 -0.76172,2.92968 v 2.22657 h 3.61328 v 2.79297 h -3.61328 v 18.33984 z" />
+ d="m 292.02058,164.95551 q 0.76172,0 1.44532,-0.23438 0.68359,-0.23437 1.21093,-0.72266 0.52735,-0.50781 0.83985,-1.26953 0.33203,-0.78125 0.39062,-1.875 h 3.26172 q -0.0586,1.58203 -0.66406,2.89063 -0.58594,1.28906 -1.5625,2.22656 -0.95703,0.91797 -2.22656,1.42578 -1.26953,0.50781 -2.69532,0.50781 -2.05078,0 -3.57421,-0.66406 -1.50391,-0.66406 -2.5,-1.91406 -0.9961,-1.25 -1.48438,-3.06641 -0.48828,-1.8164 -0.48828,-4.12109 v -2.38281 q 0,-2.30469 0.48828,-4.10157 0.48828,-1.8164 1.48438,-3.0664 0.99609,-1.26953 2.5,-1.9336 1.5039,-0.66406 3.55468,-0.66406 1.60157,0 2.89063,0.52735 1.30859,0.50781 2.22656,1.5039 0.9375,0.97656 1.46484,2.40235 0.52735,1.42578 0.58594,3.22265 h -3.26172 q -0.11718,-2.34375 -1.13281,-3.51562 -0.99609,-1.17188 -2.77344,-1.17188 -1.36718,0 -2.24609,0.48828 -0.87891,0.48828 -1.40625,1.38672 -0.50781,0.87891 -0.72266,2.12891 -0.19531,1.25 -0.19531,2.79297 v 2.38281 q 0,1.5625 0.19531,2.83203 0.19532,1.25 0.70313,2.14844 0.52734,0.8789 1.40625,1.36719 0.89844,0.46875 2.28515,0.46875 z" />
+ d="m 313.97371,165.65863 q -0.82031,1.07422 -2.07031,1.66016 -1.23047,0.58593 -2.94922,0.58593 -1.3086,0 -2.38281,-0.42968 -1.07422,-0.44922 -1.83594,-1.38672 -0.76172,-0.95703 -1.19141,-2.44141 -0.41015,-1.5039 -0.41015,-3.61328 v -13.65234 h 3.4375 v 13.6914 q 0,1.42578 0.2539,2.36328 0.27344,0.91797 0.6836,1.46485 0.42968,0.52734 0.95703,0.74219 0.54687,0.19531 1.07422,0.19531 1.71875,0 2.7539,-0.76172 1.03516,-0.76172 1.58203,-2.05078 v -15.64453 h 3.45703 v 21.13281 h -3.28125 z" />
+ d="m 331.29793,149.62347 q -0.41016,-0.0781 -0.76172,-0.0976 -0.35156,-0.0391 -0.80078,-0.0391 -1.40625,0 -2.32422,0.78125 -0.89844,0.78125 -1.38672,2.1289 v 15.11719 h -3.45703 v -21.13281 h 3.35937 l 0.0586,2.14843 q 0.64453,-1.17187 1.60156,-1.85546 0.97656,-0.6836 2.32422,-0.6836 0.15625,0 0.35156,0.0391 0.21485,0.0195 0.41016,0.0586 0.19531,0.0391 0.35156,0.0976 0.17578,0.0391 0.25391,0.0781 z" />
+ d="m 343.48543,149.62347 q -0.41016,-0.0781 -0.76172,-0.0976 -0.35156,-0.0391 -0.80078,-0.0391 -1.40625,0 -2.32422,0.78125 -0.89844,0.78125 -1.38672,2.1289 v 15.11719 h -3.45703 v -21.13281 h 3.35937 l 0.0586,2.14843 q 0.64453,-1.17187 1.60156,-1.85546 0.97656,-0.6836 2.32422,-0.6836 0.15625,0 0.35156,0.0391 0.21485,0.0195 0.41016,0.0586 0.19531,0.0391 0.35156,0.0976 0.17578,0.0391 0.25391,0.0781 z" />
+ d="m 353.11433,161.77191 3.84766,-15.39062 h 3.67188 l -6.99219,24.39453 q -0.23438,0.78125 -0.66406,1.69922 -0.41016,0.91797 -1.05469,1.69922 -0.64453,0.80078 -1.5625,1.32812 -0.89844,0.54688 -2.08985,0.54688 -0.19531,0 -0.44921,-0.0391 -0.25391,-0.0391 -0.50782,-0.0976 -0.2539,-0.0391 -0.48828,-0.0977 -0.21484,-0.0391 -0.35156,-0.0781 v -2.92969 q 0.11719,0.0391 0.37109,0.0586 0.27344,0.0195 0.39063,0.0195 0.76172,0 1.34765,-0.17578 0.58594,-0.17578 1.03516,-0.58594 0.46875,-0.39062 0.80078,-1.05468 0.35156,-0.66407 0.625,-1.64063 l 0.60547,-2.08984 -6.21094,-20.95703 h 3.76953 z" />
(C) DAVID LETTIER
+ x="-160.13664"
+ y="380.55551">(C) DAVID LETTIER
Gifcurry
+ x="236.35316"
+ y="-52.491615"
+ style="font-size:40px;line-height:1.25">Gifcurry
diff --git a/logo/logo-2-dark-theme.svg b/logo/logo-2-dark-theme.svg
index 1c12957..da7dcf4 100644
--- a/logo/logo-2-dark-theme.svg
+++ b/logo/logo-2-dark-theme.svg
@@ -6,44 +6,11 @@
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- id="svg8"
- version="1.1"
- viewBox="0 0 190.297 54.21814"
- height="54.21814mm"
width="190.297mm"
- sodipodi:docname="logo-2-dark-theme.svg"
- inkscape:version="0.92.1 r"
- inkscape:export-filename="/home/lettier/github/gifcurry/logo/logo-2.png"
- inkscape:export-xdpi="119.93034"
- inkscape:export-ydpi="119.93034">
-
+ height="54.21814mm"
+ viewBox="0 0 190.297 54.21814"
+ version="1.1"
+ id="svg8">
+ id="g4638">
+ d="M 30.00902,4.9971347 A 25.513393,25.513393 0 0 0 14.616162,10.591115 c 0.133368,0.18103 0.263304,0.36156 0.393258,0.54208 A 25.513393,25.513393 0 0 1 30.693217,5.0178047 25.513393,25.513393 0 0 0 30.00902,4.9971047 Z M 11.255127,13.816235 a 25.513393,25.513393 0 0 0 -6.250264,17.18603 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 0.382921,-0.0207 25.513393,25.513393 0 0 1 -25.321967,-24.99124 25.513393,25.513393 0 0 1 5.66632,-16.52147 c -0.155829,-0.22395 -0.313022,-0.44708 -0.474389,-0.66456 z"
+ id="path6070" />
+ d="m 32.584056,4.9971347 a 25.513393,25.513393 0 0 0 -0.433565,0.0238 25.513393,25.513393 0 0 1 25.219134,24.9726403 l 5.3e-4,0.031 a 25.513393,25.513393 0 0 1 -5.50561,16.28325 c 0.1834,0.26966 0.3692,0.53514 0.56018,0.79478 a 25.513393,25.513393 0 0 0 6.17275,-17.07803 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 32.584056,4.9971347 Z M 48.613575,49.702415 a 25.513393,25.513393 0 0 1 -15.819712,6.28747 25.513393,25.513393 0 0 0 0.783415,0.0243 25.513393,25.513393 0 0 0 15.502927,-5.66839 c -0.1577,-0.21469 -0.31368,-0.42907 -0.46663,-0.64337 z"
+ id="path6068" />
+ d="M 31.59652,4.9971347 A 25.513393,25.513393 0 0 0 15.533936,11.126475 c 3.454616,4.79733 5.736982,9.58836 8.496639,23.93859 l 7.284826,-24.57937 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534349,4.71687 12.525849,13.20694 3.15251,13.38741 5.09705,18.65842 7.79436,22.62188 a 25.513393,25.513393 0 0 0 5.50095,-16.28997 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 31.59652,4.9971347 Z M 12.254549,14.474595 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 16.268773,-6.304 c -3.38483,-4.74042 -5.64381,-9.58763 -8.370031,-23.76393 l -7.28431,24.57938 -0.451652,-0.11162 -0.02222,0.11162 c -7.861171,-1.46742 -10.534351,-4.71687 -12.52585,-13.20695 -3.212296,-13.64125 -5.171564,-18.85363 -7.949903,-22.84408 z m 22.52679,0.85214 -7.606255,23.40684 H 25.02638 c 0.8393,3.5098 2.013317,5.65789 4.711855,6.95152 l 7.605739,-23.40736 h 2.149221 c -0.839229,-3.50953 -2.013754,-5.65733 -4.711856,-6.951 z"
+ style="display:inline;opacity:1;fill:#1b59a2;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ d="M 31.350966,4.9971347 A 25.513393,25.513393 0 0 0 15.288382,11.126475 c 3.454616,4.79733 5.736982,9.58836 8.496639,23.93859 l 7.284826,-24.57937 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534343,4.71687 12.525843,13.20694 3.15251,13.38741 5.09705,18.65842 7.79436,22.62188 a 25.513393,25.513393 0 0 0 5.50095,-16.28997 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 31.350966,4.9971347 Z M 12.008995,14.474595 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 16.268767,-6.304 c -3.38483,-4.74042 -5.64381,-9.58763 -8.370025,-23.76393 l -7.28431,24.57938 -0.451652,-0.11162 -0.02222,0.11162 c -7.861171,-1.46742 -10.534351,-4.71687 -12.52585,-13.20695 -3.212296,-13.64125 -5.171564,-18.85363 -7.949903,-22.84408 z m 22.52679,0.85214 -7.606255,23.40684 h -2.148704 c 0.8393,3.5098 2.013317,5.65789 4.711855,6.95152 l 7.605739,-23.40736 h 2.149221 c -0.839229,-3.50953 -2.013754,-5.65733 -4.711856,-6.951 z"
+ id="path4681" />
+ d="M 31.067353,4.9971347 A 25.513393,25.513393 0 0 0 15.004769,11.126475 c 3.454616,4.79733 5.736982,9.58836 8.496639,23.93859 l 7.284826,-24.57937 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534346,4.71687 12.525846,13.20694 3.15251,13.38741 5.09705,18.65842 7.79436,22.62188 a 25.513393,25.513393 0 0 0 5.50095,-16.28997 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 31.067353,4.9971347 Z M 11.725382,14.474595 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 16.26877,-6.304 c -3.38483,-4.74042 -5.64381,-9.58763 -8.370028,-23.76393 l -7.28431,24.57938 -0.451652,-0.11162 -0.02222,0.11162 c -7.861171,-1.46742 -10.534351,-4.71687 -12.52585,-13.20695 -3.212296,-13.64125 -5.171564,-18.85363 -7.949903,-22.84408 z m 22.52679,0.85214 -7.606255,23.40684 h -2.148704 c 0.8393,3.5098 2.013317,5.65789 4.711855,6.95152 l 7.605739,-23.40736 h 2.149221 c -0.839229,-3.50953 -2.013754,-5.65733 -4.711856,-6.951 z"
+ id="path4648" />
+ Gifcurr
+ λ
Gifcurr
λ
- Gifcurr
λ
- λ
+ id="tspan6123">λ
Gifcurr
+ y="34.74176"
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;writing-mode:lr-tb;text-anchor:start;fill:#dfdfdf;fill-opacity:1;stroke-width:0.26458332px;">Gifcurr
+ transform="translate(-3.1131187,-3.9921436)"
+ id="g4644">
+ id="path4614" />
© 2018 David Lettier
+ id="tspan4585">© 2018 David Lettier
diff --git a/logo/logo-2.svg b/logo/logo-2.svg
index 9f0e675..3d75c39 100644
--- a/logo/logo-2.svg
+++ b/logo/logo-2.svg
@@ -6,44 +6,11 @@
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- id="svg8"
- version="1.1"
- viewBox="0 0 190.297 54.21814"
- height="54.21814mm"
width="190.297mm"
- sodipodi:docname="logo-2.svg"
- inkscape:version="0.92.1 r"
- inkscape:export-filename="/home/lettier/github/gifcurry/logo/logo-2.png"
- inkscape:export-xdpi="119.93034"
- inkscape:export-ydpi="119.93034">
-
+ height="54.21814mm"
+ viewBox="0 0 190.297 54.21814"
+ version="1.1"
+ id="svg8">
+ id="g4638">
+ d="M 30.00902,4.9971347 A 25.513393,25.513393 0 0 0 14.616162,10.591115 c 0.133368,0.18103 0.263304,0.36156 0.393258,0.54208 A 25.513393,25.513393 0 0 1 30.693217,5.0178047 25.513393,25.513393 0 0 0 30.00902,4.9971047 Z M 11.255127,13.816235 a 25.513393,25.513393 0 0 0 -6.250264,17.18603 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 0.382921,-0.0207 25.513393,25.513393 0 0 1 -25.321967,-24.99124 25.513393,25.513393 0 0 1 5.66632,-16.52147 c -0.155829,-0.22395 -0.313022,-0.44708 -0.474389,-0.66456 z"
+ id="path6070" />
+ d="m 32.584056,4.9971347 a 25.513393,25.513393 0 0 0 -0.433565,0.0238 25.513393,25.513393 0 0 1 25.219134,24.9726403 l 5.3e-4,0.031 a 25.513393,25.513393 0 0 1 -5.50561,16.28325 c 0.1834,0.26966 0.3692,0.53514 0.56018,0.79478 a 25.513393,25.513393 0 0 0 6.17275,-17.07803 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 32.584056,4.9971347 Z M 48.613575,49.702415 a 25.513393,25.513393 0 0 1 -15.819712,6.28747 25.513393,25.513393 0 0 0 0.783415,0.0243 25.513393,25.513393 0 0 0 15.502927,-5.66839 c -0.1577,-0.21469 -0.31368,-0.42907 -0.46663,-0.64337 z"
+ id="path6068" />
+ d="M 31.59652,4.9971347 A 25.513393,25.513393 0 0 0 15.533936,11.126475 c 3.454616,4.79733 5.736982,9.58836 8.496639,23.93859 l 7.284826,-24.57937 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534349,4.71687 12.525849,13.20694 3.15251,13.38741 5.09705,18.65842 7.79436,22.62188 a 25.513393,25.513393 0 0 0 5.50095,-16.28997 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 31.59652,4.9971347 Z M 12.254549,14.474595 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 16.268773,-6.304 c -3.38483,-4.74042 -5.64381,-9.58763 -8.370031,-23.76393 l -7.28431,24.57938 -0.451652,-0.11162 -0.02222,0.11162 c -7.861171,-1.46742 -10.534351,-4.71687 -12.52585,-13.20695 -3.212296,-13.64125 -5.171564,-18.85363 -7.949903,-22.84408 z m 22.52679,0.85214 -7.606255,23.40684 H 25.02638 c 0.8393,3.5098 2.013317,5.65789 4.711855,6.95152 l 7.605739,-23.40736 h 2.149221 c -0.839229,-3.50953 -2.013754,-5.65733 -4.711856,-6.951 z"
+ style="display:inline;opacity:1;fill:#1b59a2;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ d="M 31.350966,4.9971347 A 25.513393,25.513393 0 0 0 15.288382,11.126475 c 3.454616,4.79733 5.736982,9.58836 8.496639,23.93859 l 7.284826,-24.57937 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534343,4.71687 12.525843,13.20694 3.15251,13.38741 5.09705,18.65842 7.79436,22.62188 a 25.513393,25.513393 0 0 0 5.50095,-16.28997 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 31.350966,4.9971347 Z M 12.008995,14.474595 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 16.268767,-6.304 c -3.38483,-4.74042 -5.64381,-9.58763 -8.370025,-23.76393 l -7.28431,24.57938 -0.451652,-0.11162 -0.02222,0.11162 c -7.861171,-1.46742 -10.534351,-4.71687 -12.52585,-13.20695 -3.212296,-13.64125 -5.171564,-18.85363 -7.949903,-22.84408 z m 22.52679,0.85214 -7.606255,23.40684 h -2.148704 c 0.8393,3.5098 2.013317,5.65789 4.711855,6.95152 l 7.605739,-23.40736 h 2.149221 c -0.839229,-3.50953 -2.013754,-5.65733 -4.711856,-6.951 z"
+ id="path4681" />
+ d="M 31.067353,4.9971347 A 25.513393,25.513393 0 0 0 15.004769,11.126475 c 3.454616,4.79733 5.736982,9.58836 8.496639,23.93859 l 7.284826,-24.57937 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534346,4.71687 12.525846,13.20694 3.15251,13.38741 5.09705,18.65842 7.79436,22.62188 a 25.513393,25.513393 0 0 0 5.50095,-16.28997 l -5.3e-4,-0.031 A 25.513393,25.513393 0 0 0 31.067353,4.9971347 Z M 11.725382,14.474595 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.997379,25.01191 25.513393,25.513393 0 0 0 16.26877,-6.304 c -3.38483,-4.74042 -5.64381,-9.58763 -8.370028,-23.76393 l -7.28431,24.57938 -0.451652,-0.11162 -0.02222,0.11162 c -7.861171,-1.46742 -10.534351,-4.71687 -12.52585,-13.20695 -3.212296,-13.64125 -5.171564,-18.85363 -7.949903,-22.84408 z m 22.52679,0.85214 -7.606255,23.40684 h -2.148704 c 0.8393,3.5098 2.013317,5.65789 4.711855,6.95152 l 7.605739,-23.40736 h 2.149221 c -0.839229,-3.50953 -2.013754,-5.65733 -4.711856,-6.951 z"
+ id="path4648" />
- Gifcurr
- λ
+ id="g4631">
Gifcurr
λ
+ Gifcurr
λ
+ x="173.11317"
+ id="tspan6154">λ
λ
+ Gifcurr
+ x="61.230457"
+ id="tspan6127">Gifcurr
+ transform="translate(-3.1131187,-3.9921436)"
+ id="g4644">
+ id="path4614" />
© 2018 David Lettier
+ id="tspan4585">© 2018 David Lettier
diff --git a/logo/logo-3.svg b/logo/logo-3.svg
index b96761c..325c746 100644
--- a/logo/logo-3.svg
+++ b/logo/logo-3.svg
@@ -5,39 +5,11 @@
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
id="svg8"
version="1.1"
viewBox="0 0 191.61173 55.272122"
height="55.272121mm"
- width="191.61172mm"
- sodipodi:docname="logo-3.svg"
- inkscape:version="0.92.2 2405546, 2018-03-11">
-
+ width="191.61172mm">
@@ -98,213 +66,179 @@
+
+
+ (C) 2018 David Lettier
+
+
+ style="display:inline;fill:#3e2a69;fill-opacity:0.39215687;filter:url(#filter912)">
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#3e2a69;fill-opacity:0.39215687;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path961" />
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#3e2a69;fill-opacity:0.39215687;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path963" />
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#3e2a69;fill-opacity:0.39215687;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path965" />
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#3e2a69;fill-opacity:0.39215687;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path967" />
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#3e2a69;fill-opacity:0.39215687;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path969" />
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#3e2a69;fill-opacity:0.39215687;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path971" />
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#3e2a69;fill-opacity:0.39215687;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path973" />
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#3e2a69;fill-opacity:0.39215687;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path975" />
+ id="path1063" />
+ id="path1067" />
+ id="path1065" />
+ id="path1069" />
+ id="path1031" />
+ id="path1033" />
+ id="path1035" />
+ id="path1037" />
+ d="m 175.8193,50.5131 -0.65036,-3.696788 c 3.49141,-0.547672 4.58675,-1.471869 5.57941,-4.073312 h -1.91685 l -5.51095,-17.525514 5.3398,-1.232262 3.32027,14.616004 c 1.30071,-7.87279 2.39606,-11.090364 4.58675,-14.616004 l 5.20288,1.711476 c -2.43029,3.286034 -3.55987,6.708986 -5.71633,15.848266 -1.47186,6.36669 -4.34714,8.112396 -10.23462,8.968134 z" />
+ d="m 77.281721,18.440243 c -6.36669,0 -11.364199,4.381379 -11.364199,12.425315 0,8.112396 3.628328,12.459544 10.884986,12.459544 2.977968,0 6.058625,-0.924197 8.52315,-2.498754 V 29.085624 h -9.070822 l 0.547672,3.970624 h 3.080656 v 5.374034 c -0.992655,0.547672 -2.05377,0.787279 -3.217574,0.787279 -3.286033,0 -4.929051,-2.12223 -4.929051,-8.352003 0,-5.887477 2.464526,-8.352002 5.784789,-8.352002 1.916853,0 3.217575,0.581902 4.826362,1.814164 l 2.84105,-2.977967 c -1.951082,-1.711476 -4.449837,-2.90951 -7.907019,-2.90951 z" />
+ d="m 91.642074,14.983062 c -1.882624,0 -3.183345,1.334951 -3.183345,3.080657 0,1.745706 1.300721,3.080656 3.183345,3.080656 1.882624,0 3.217574,-1.33495 3.217574,-3.080656 0,-1.745706 -1.33495,-3.080657 -3.217574,-3.080657 z m 2.738361,9.584265 h -5.408264 v 18.175874 h 5.408264 z" />
+ d="m 106.917,20.76785 c 0.78728,0 1.81416,0.171148 2.84105,0.684591 l 1.47187,-3.52564 c -1.33495,-0.684591 -3.08066,-1.163804 -4.96328,-1.163804 -4.65522,0 -6.948596,2.704132 -6.948596,6.195543 v 1.608787 h -2.738361 v 3.765247 h 2.738361 v 14.410627 h 5.408266 V 28.332574 h 3.69679 l 0.5819,-3.765247 h -4.27869 v -1.40341 c 0,-1.711476 0.61613,-2.396067 2.19069,-2.396067 z" />
+ d="m 118.30527,23.985426 c -5.4425,0 -8.89968,4.039082 -8.89968,9.823871 0,5.750559 3.42295,9.515805 9.00236,9.515805 2.49876,0 4.44984,-0.821507 6.12709,-2.156459 L 122.1732,37.81415 c -1.30072,0.821509 -2.19069,1.232263 -3.49141,1.232263 -2.15646,0 -3.5941,-1.232263 -3.5941,-5.271346 0,-4.073312 1.33495,-5.64787 3.66256,-5.64787 1.23226,0 2.29338,0.410754 3.45718,1.300721 l 2.32761,-3.217574 c -1.74571,-1.471869 -3.69679,-2.224918 -6.22977,-2.224918 z" />
+ d="m 141.81239,24.567327 h -5.40827 v 12.630692 c -0.78728,1.369181 -1.81416,2.12223 -2.94374,2.12223 -1.12957,0 -1.74571,-0.547672 -1.74571,-2.396066 V 24.567327 h -5.40825 v 13.041446 c 0,3.525641 1.67724,5.716329 5.06597,5.716329 2.39606,0 4.176,-0.958425 5.51095,-2.875279 l 0.2396,2.293378 h 4.68945 z" />
+ d="m 155.92404,24.053884 c -2.088,0 -3.86793,1.506099 -4.68944,4.039083 l -0.44499,-3.52564 h -4.72367 v 18.175874 h 5.40827 v -9.002364 c 0.5819,-2.772591 1.50609,-4.449837 3.76524,-4.449837 0.61613,0 1.06111,0.102689 1.64301,0.239607 l 0.85575,-5.237116 c -0.5819,-0.171147 -1.12958,-0.239607 -1.81417,-0.239607 z" />
+ d="m 169.62922,24.053884 c -2.088,0 -3.86794,1.506099 -4.68945,4.039083 l -0.44498,-3.52564 h -4.72367 v 18.175874 h 5.40826 v -9.002364 c 0.5819,-2.772591 1.5061,-4.449837 3.76525,-4.449837 0.61613,0 1.06112,0.102689 1.64302,0.239607 l 0.85573,-5.237116 c -0.5819,-0.171147 -1.12956,-0.239607 -1.81416,-0.239607 z" />
+ id="path1029" />
+ id="path1039" />
+ id="path1041" />
+ id="path1043" />
+ id="path1053" />
+ id="path1045" />
+ id="path1047" />
+ id="path1049" />
+ id="path1051" />
+ id="path1055" />
+ id="path1057" />
+ id="path1059" />
+ id="path1061" />
+ style="display:inline;opacity:1;vector-effect:none;fill:#491ea4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.32291663;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1107.35998535;stroke-opacity:1" />
+ style="display:inline;opacity:1;fill:#3c147c;fill-opacity:0.60000002;fill-rule:nonzero;stroke:none;stroke-width:0.58234793;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;filter:url(#filter948)" />
+ id="path884" />
+ id="path878" />
+ id="path886" />
+ id="path888" />
+ id="path890" />
+ id="path892" />
+ id="path894" />
+ id="path851" />
+ id="path4934" />
+ id="path4944" />
+ id="path4946" />
+ id="path4948" />
+ id="path4950" />
+ id="path4952" />
+ id="path4954" />
+ id="path4942" />
diff --git a/makefile b/makefile
index dfe3630..c82cc84 100644
--- a/makefile
+++ b/makefile
@@ -3,61 +3,70 @@
.RECIPEPREFIX != ps
-STACK=stack --allow-different-user
-STACK_PATH_LOCAL_BIN=`$(STACK) path --local-bin`
-STACK_GHC_EXE=`$(STACK) path --compiler-exe`
-STACK_GHC_BIN=`$(STACK) path --compiler-bin`
-STACK_PATHS=$(STACK_PATH_LOCAL_BIN):$(STACK_GHC_BIN)
-CABAL=env PATH=$(PATH):$(STACK_PATHS) $(STACK_PATH_LOCAL_BIN)/cabal
-CABAL_SANDBOX_DIR=".cabal-sandbox"
-_APPLICATIONS_DESKTOP_DIR="$(CABAL_SANDBOX_DIR)/share/applications"
-_ICONS_HICOLOR_SCALABLE_APPS_DIR="$(CABAL_SANDBOX_DIR)/share/icons/hicolor/scalable/apps"
+_GIFCURRY_VERSION="4.0.0.0"
+_STACK=stack --allow-different-user
+_GHC_VERSION=`$(_STACK) ghc -- --version | sed 's|The Glorious Glasgow Haskell Compilation System, version ||g'`
+_STACK_PATH_LOCAL_BIN=`$(_STACK) path --local-bin`
+_STACK_GHC_EXE=`$(_STACK) path --compiler-exe`
+_STACK_GHC_BIN=`$(_STACK) path --compiler-bin`
+_STACK_PATHS=$(_STACK_PATH_LOCAL_BIN):$(_STACK_GHC_BIN)
+_CABAL=env PATH=$(PATH):$(_STACK_PATHS) $(_STACK_PATH_LOCAL_BIN)/cabal
+_CABAL_SANDBOX_DIR=".cabal-sandbox"
+_METAINFO_DIR="$(_CABAL_SANDBOX_DIR)/share/metainfo"
+_APPLICATIONS_DESKTOP_DIR="$(_CABAL_SANDBOX_DIR)/share/applications"
+_ICONS_HICOLOR_SCALABLE_APPS_DIR="$(_CABAL_SANDBOX_DIR)/share/icons/hicolor/scalable/apps"
_PACKAGING_LINUX_COMMON_DIR="./packaging/linux/common"
-VERSION='3.0.0.2'
+_GIFCURRY_LINUX_PACKAGE_DIR="gifcurry-linux-$(_GIFCURRY_VERSION)"
-export PATH := $(PATH):$(STACK_PATH_LOCAL_BIN)
+export PATH := $(PATH):$(_STACK_PATH_LOCAL_BIN)
all: setup update sandbox_clean clean alex happy haskell_gi gtk2hs_buildtools install_dependencies configure build cabal_install
-setup:
- $(STACK) setup && $(STACK) update && \
- $(STACK) install Cabal && \
- $(STACK) install cabal-install
+none:
+
+setup: none
+ $(_STACK) setup && $(_STACK) update && \
+ $(_STACK) install Cabal && \
+ $(_STACK) install cabal-install
alex: setup
- $(STACK) install alex
+ $(_STACK) install alex
happy: setup
- $(STACK) install happy
+ $(_STACK) install happy
haskell_gi: setup
- $(STACK) install haskell-gi
+ $(_STACK) install haskell-gi
gtk2hs_buildtools: setup
- $(STACK) install gtk2hs-buildtools
+ $(_STACK) install gtk2hs-buildtools
sandbox: setup
- $(CABAL) sandbox init
+ $(_CABAL) sandbox init
clean: setup
- $(CABAL) clean
+ $(_CABAL) clean
check: setup
- $(CABAL) check
+ $(_CABAL) check
sandbox_clean: setup
- $(CABAL) sandbox init && $(CABAL) sandbox delete && $(CABAL) sandbox init
+ $(_CABAL) sandbox init && $(_CABAL) sandbox delete && $(_CABAL) sandbox init
update: sandbox
- $(CABAL) --require-sandbox update
+ $(_CABAL) --require-sandbox update
install_dependencies: sandbox
- $(CABAL) --require-sandbox install -j -w $(STACK_GHC_EXE) --only-dependencies
+ $(_CABAL) --require-sandbox install -j -w $(_STACK_GHC_EXE) --only-dependencies
configure: sandbox
- $(CABAL) --require-sandbox configure -w $(STACK_GHC_EXE)
+ $(_CABAL) --require-sandbox configure -w $(_STACK_GHC_EXE)
+
+appdata_xml: sandbox
+ mkdir -p $(_METAINFO_DIR) && \
+ cp $(_PACKAGING_LINUX_COMMON_DIR)/com.lettier.gifcurry.appdata.xml $(_METAINFO_DIR)/
-applications_desktop: sandbox
+applications_desktop: appdata_xml
mkdir -p $(_APPLICATIONS_DESKTOP_DIR) && \
cp $(_PACKAGING_LINUX_COMMON_DIR)/com.lettier.gifcurry.desktop $(_APPLICATIONS_DESKTOP_DIR)/
@@ -66,13 +75,33 @@ icons_hicolor_scalable_apps: applications_desktop
cp $(_PACKAGING_LINUX_COMMON_DIR)/com.lettier.gifcurry.svg $(_ICONS_HICOLOR_SCALABLE_APPS_DIR)/
build: configure
- $(CABAL) --require-sandbox build -j
-
-cabal_install: applications_desktop icons_hicolor_scalable_apps build
- $(CABAL) --require-sandbox install -j -w $(STACK_GHC_EXE) --enable-relocatable
+ $(_CABAL) --require-sandbox build -j
+
+cabal_install: appdata_xml applications_desktop icons_hicolor_scalable_apps build
+ $(_CABAL) --require-sandbox install -j -w $(_STACK_GHC_EXE) --enable-relocatable
+
+package_cabal_sandbox_for_linux: cabal_install
+ rm -rf "._gifcurry_trash_" && \
+ mkdir -p "._gifcurry_trash_" && \
+ mkdir -p $(_GIFCURRY_LINUX_PACKAGE_DIR) && \
+ touch "$(_GIFCURRY_LINUX_PACKAGE_DIR).tar.gz" && \
+ mv "$(_GIFCURRY_LINUX_PACKAGE_DIR).tar.gz" "._gifcurry_trash_/" && \
+ mv $(_GIFCURRY_LINUX_PACKAGE_DIR) "._gifcurry_trash_" && \
+ mkdir -p $(_GIFCURRY_LINUX_PACKAGE_DIR) && \
+ cp -R "$(_CABAL_SANDBOX_DIR)/." $(_GIFCURRY_LINUX_PACKAGE_DIR) && \
+ find "$(_GIFCURRY_LINUX_PACKAGE_DIR)/share/x86_64-linux-ghc-$(_GHC_VERSION)/" -mindepth 1 -maxdepth 1 -type d \
+ -not -path '*Gifcurry*' -exec mv {} "._gifcurry_trash_/" \; && \
+ find "$(_GIFCURRY_LINUX_PACKAGE_DIR)/lib/x86_64-linux-ghc-$(_GHC_VERSION)/" -mindepth 1 -maxdepth 1 -type d \
+ -exec mv {} "._gifcurry_trash_/" \; && \
+ find "$(_GIFCURRY_LINUX_PACKAGE_DIR)/bin/" -type f -not -name '*gifcurry*' -exec mv {} "._gifcurry_trash_/" \; && \
+ find "$(_GIFCURRY_LINUX_PACKAGE_DIR)/" -mindepth 1 -maxdepth 1 -type d -not -path '*bin*' -not -path '*lib*' -not -path '*share*' \
+ -exec mv {} "._gifcurry_trash_/" \; && \
+ find "$(_GIFCURRY_LINUX_PACKAGE_DIR)/" -mindepth 1 -maxdepth 1 -type f -not -path '*bin*' -not -path '*lib*' -not -path '*share*' \
+ -exec mv {} "._gifcurry_trash_/" \; && \
+ tar -zcvf "$(_GIFCURRY_LINUX_PACKAGE_DIR).tar.gz" $(_GIFCURRY_LINUX_PACKAGE_DIR)
release: check build
- $(CABAL) sdist
+ $(_CABAL) sdist
run_gui: cabal_install
./.cabal-sandbox/bin/gifcurry_gui
@@ -81,10 +110,10 @@ run_cli: cabal_install
./.cabal-sandbox/bin/gifcurry_cli $(CLI_ARGS)
build_docs: setup
- $(CABAL) haddock --hyperlink-source \
+ $(_CABAL) haddock --hyperlink-source \
--html-location='http://hackage.haskell.org/package/Gifcurry/docs' \
--contents-location='http://hackage.haskell.org/package/Gifcurry' && \
mkdir -p ./haddock && \
- cp -R ./dist/doc/html/Gifcurry/ ./haddock/Gifcurry-$(VERSION)-docs && \
+ cp -R ./dist/doc/html/Gifcurry/ ./haddock/Gifcurry-$(_GIFCURRY_VERSION)-docs && \
cd ./haddock && \
- tar --format=ustar -cvf ./Gifcurry-$(VERSION)-docs.tar Gifcurry-$(VERSION)-docs
+ tar --format=ustar -cvf ./Gifcurry-$(_GIFCURRY_VERSION)-docs.tar Gifcurry-$(_GIFCURRY_VERSION)-docs
diff --git a/packaging/linux/app-image/gifcurry-app-image-install.sh b/packaging/linux/app-image/gifcurry-app-image-install.sh
index a46b34b..8128110 100755
--- a/packaging/linux/app-image/gifcurry-app-image-install.sh
+++ b/packaging/linux/app-image/gifcurry-app-image-install.sh
@@ -3,7 +3,7 @@
# (C) 2017 David Lettier
# lettier.com
-GIFCURRY_VERSION="3.0.0.2"
+GIFCURRY_VERSION="4.0.0.0"
GIFCURRY_RELEASES_DOWNLOAD="https://github.com/lettier/gifcurry/releases/download/$GIFCURRY_VERSION"
GIFCURRY_PACKAGING_LINUX_COMMON="https://raw.githubusercontent.com/lettier/gifcurry/master/packaging/linux/common"
GIFCURRY_APP_IMAGE="gifcurry-$GIFCURRY_VERSION-x86_64.AppImage"
diff --git a/packaging/linux/arch-aur/PKGBUILD b/packaging/linux/arch-aur/PKGBUILD
index e97cf78..edede83 100755
--- a/packaging/linux/arch-aur/PKGBUILD
+++ b/packaging/linux/arch-aur/PKGBUILD
@@ -1,7 +1,7 @@
# Maintainer: Lettier
_name=gifcurry
-_ver=3.0.0.2
+_ver=4.0.0.0
_xrev=0
pkgname=${_name}
diff --git a/packaging/linux/app-image/com.lettier.gifcurry.appdata.xml b/packaging/linux/common/com.lettier.gifcurry.appdata.xml
similarity index 77%
rename from packaging/linux/app-image/com.lettier.gifcurry.appdata.xml
rename to packaging/linux/common/com.lettier.gifcurry.appdata.xml
index 26d2723..4c8084b 100644
--- a/packaging/linux/app-image/com.lettier.gifcurry.appdata.xml
+++ b/packaging/linux/common/com.lettier.gifcurry.appdata.xml
@@ -1,7 +1,7 @@
- com.lettier.gifcurry.desktop
+ com.lettier.gifcurry
CC-BY-SA-3.0
BSD-3-Clause
Gifcurry
@@ -17,12 +17,9 @@
com.lettier.gifcurry.desktop
- The main Gifcurry window
- https://i.imgur.com/Xw5h21W.png
+ Gifcurry
+ https://i.imgur.com/NG29XOB.png
https://github.com/lettier/gifcurry
-
- gifcurry_gui
-
diff --git a/packaging/linux/snap/snapcraft.yaml b/packaging/linux/snap/snapcraft.yaml
index e2ebb71..7b2f690 100644
--- a/packaging/linux/snap/snapcraft.yaml
+++ b/packaging/linux/snap/snapcraft.yaml
@@ -1,5 +1,5 @@
name: gifcurry
-version: '3.0.0.2'
+version: '4.0.0.0'
summary: Your open source video to GIF maker.
type: app
description: |
diff --git a/packaging/mac/gifcurry-mac-install-script.command b/packaging/mac/gifcurry-mac-install-script.command
index 05c76fc..cfdbe27 100644
--- a/packaging/mac/gifcurry-mac-install-script.command
+++ b/packaging/mac/gifcurry-mac-install-script.command
@@ -16,6 +16,7 @@ brew install \
libav \
libogg \
libvorbis \
+ libvpx \
pkg-config \
gobject-introspection \
cairo \
@@ -26,17 +27,21 @@ brew install \
gnome-icon-theme \
openh264 \
theora \
- ffmpeg \
imagemagick \
ghostscript \
gstreamer \
gst-libav \
gst-plugins-base \
gst-plugins-good
+brew install ffmpeg --with-libvpx
brew install --with-gtk+3 gst-plugins-bad
wget -qO- https://get.haskellstack.org/ | sh -s - -f
+cd $HOME/Downloads/
git clone https://github.com/lettier/gifcurry.git
cd gifcurry/
+git pull
+git reset --hard origin/master
+git pull
LIBFFIPKGCONFIG=`find /usr/local/Cellar -path '*libffi*' -type d -name 'pkgconfig' 2>/dev/null | tr '\n' ':' | sed 's/:$//'`
export PKG_CONFIG_PATH=$PKG_CONFIG_PATH:$LIBFFIPKGCONFIG
stack setup
diff --git a/src/cli/Main.hs b/src/cli/Main.hs
index 311dbf9..f8a1086 100644
--- a/src/cli/Main.hs
+++ b/src/cli/Main.hs
@@ -6,34 +6,81 @@
{-# LANGUAGE
DeriveDataTypeable
+ , OverloadedStrings
, NamedFieldPuns
#-}
{-# OPTIONS_GHC -fno-cse #-}
-import Control.Monad
+import System.Directory
import System.Console.CmdArgs
+import Control.Monad
+import Data.Text (pack, unpack, strip)
+import Data.Maybe
+import Data.Yaml
+import qualified Data.ByteString.Char8 as DBC
import qualified Gifcurry
data CliArgs =
CliArgs
- { input_file :: String
- , output_file :: String
- , save_as_video :: Bool
- , start_time :: Float
- , duration_time :: Float
- , width_size :: Int
- , quality_percent :: Float
- , font_choice :: String
- , top_text :: String
- , bottom_text :: String
- , left_crop :: Float
- , right_crop :: Float
- , top_crop :: Float
- , bottom_crop :: Float
+ { input_file :: String
+ , output_file :: String
+ , save_as_video :: Bool
+ , start_time :: Float
+ , duration_time :: Float
+ , width_size :: Int
+ , quality :: String
+ , left_crop :: Float
+ , right_crop :: Float
+ , top_crop :: Float
+ , bottom_crop :: Float
+ , text_overlays_file :: String
}
deriving (Data, Typeable, Show, Eq)
+data TextOverlay =
+ TextOverlay
+ { text :: String
+ , fontFamily :: String
+ , fontStyle :: String
+ , fontStretch :: String
+ , fontWeight :: Int
+ , fontSize :: Int
+ , origin :: String
+ , xTranslation :: Float
+ , yTranslation :: Float
+ , rotation :: Int
+ , startTime :: Float
+ , durationTime :: Float
+ , outlineSize :: Int
+ , outlineColor :: String
+ , fillColor :: String
+ }
+ deriving (Show)
+
+instance FromJSON TextOverlay where
+ parseJSON =
+ withObject
+ "TextOverlay"
+ (\ obj ->
+ TextOverlay
+ <$> obj .: "text"
+ <*> obj .:? "fontFamily" .!= "Sans"
+ <*> obj .:? "fontStyle" .!= "Normal"
+ <*> obj .:? "fontStretch" .!= "Normal"
+ <*> obj .:? "fontWeight" .!= 400
+ <*> obj .:? "fontSize" .!= 30
+ <*> obj .:? "origin" .!= "Center"
+ <*> obj .:? "xTranslation" .!= 0.0
+ <*> obj .:? "yTranslation" .!= 0.0
+ <*> obj .:? "rotation" .!= 0
+ <*> obj .: "startTime"
+ <*> obj .: "durationTime"
+ <*> obj .:? "outlineSize" .!= 10
+ <*> obj .:? "outlineColor" .!= "rgba(0,0,0)"
+ <*> obj .:? "fillColor" .!= "rgba(255,255,255)"
+ )
+
programName :: String
programName = "gifcurry_cli"
@@ -77,30 +124,13 @@ cliArgs =
= 500
&= groupname "OUTPUT FILE SIZE"
&= help "How wide the GIF needs to be. Height will scale to match."
- , quality_percent
- = 100.0
+ , quality
+ = "medium"
&= groupname "OUTPUT FILE SIZE"
&= help
- ( "From 1 (very low quality) to 100 (the best quality). "
- ++ "Controls how many colors are used and how many frames per second there are."
+ ( "Controls how many colors are used and the frame rate. \n"
+ ++ "The options are High, Medium, and Low."
)
- , font_choice
- = Gifcurry.defaultFontChoice
- &= groupname "TEXT"
- &= typ "TEXT"
- &= help "Choose your desired font for the top and bottom text."
- , top_text
- = ""
- &= groupname "TEXT"
- &= typ "TEXT"
- &= name "t"
- &= help "The text you wish to add to the top of the GIF."
- , bottom_text
- = ""
- &= groupname "TEXT"
- &= typ "TEXT"
- &= name "b"
- &= help "The text you wish to add to the bottom of the GIF."
, left_crop
= 0.0
&= groupname "CROP"
@@ -121,17 +151,77 @@ cliArgs =
&= groupname "CROP"
&= name "B"
&= help "The amount you wish to crop from the bottom."
+ , text_overlays_file
+ = ""
+ &= groupname "TEXT"
+ &= typFile
+ &= name "t"
+ &= help
+ (unlines
+ [ "The text overlays YAML file path and name."
+ , "\n"
+ , "The format is:"
+ , "\n"
+ , "- text: ..."
+ , "\n"
+ , " fontFamily: ..."
+ , "\n"
+ , " fontStyle: ..."
+ , "\n"
+ , " fontStretch: ..."
+ , "\n"
+ , " fontWeight: ..."
+ , "\n"
+ , " fontSize: ..."
+ , "\n"
+ , " origin: ..."
+ , "\n"
+ , " xTranslation: ..."
+ , "\n"
+ , " yTranslation: ..."
+ , "\n"
+ , " rotation: ..."
+ , "\n"
+ , " startTime: ..."
+ , "\n"
+ , " durationTime: ..."
+ , "\n"
+ , " outlineSize: ..."
+ , "\n"
+ , " outlineColor: ..."
+ , "\n"
+ , " fillColor: ..."
+ , "\n"
+ , "- text: ..."
+ , "\n"
+ , "..."
+ , " \n"
+ , " \n"
+ ]
+ )
}
- &= summary (info icon)
+ &= summary ""
&= program programName
&= details ["Visit https://github.com/lettier/gifcurry for more information.", ""]
main :: IO ()
main = do
- cliArgs' <- cmdArgs cliArgs
- let params = makeGifParams cliArgs'
putStrLn $ info logo
- paramsValid <- Gifcurry.gifParamsValid params
+ cliArgs' <- cmdArgs cliArgs
+ let text_overlays_file' = unpack $ strip $ pack $ text_overlays_file cliArgs'
+ textOverlays <-
+ if null text_overlays_file'
+ then return []
+ else do
+ text_overlays_file_exists <- doesFileExist text_overlays_file'
+ text_overlays_data <-
+ if text_overlays_file_exists
+ then DBC.readFile text_overlays_file'
+ else return ""
+ let maybeTextOverlays = Data.Yaml.decode text_overlays_data :: Maybe [TextOverlay]
+ makeTextOverlays text_overlays_file' maybeTextOverlays
+ let params = (makeGifParams cliArgs') { Gifcurry.textOverlays = textOverlays }
+ paramsValid <- Gifcurry.gifParamsValid params
if paramsValid
then void $ Gifcurry.gif params
else
@@ -139,6 +229,66 @@ main = do
"[INFO] Type \"" ++ programName ++ " -?\" for help."
return ()
+makeTextOverlays :: String -> Maybe [TextOverlay] -> IO [Gifcurry.TextOverlay]
+makeTextOverlays text_overlays_file' maybeTextOverlays =
+ case maybeTextOverlays of
+ Nothing -> do
+ putStrLn $
+ "[WARNING] Could not parse the " ++ text_overlays_file' ++ " YAML file!"
+ return []
+ Just textOverlays ->
+ mapM
+ (\
+ TextOverlay
+ { text
+ , fontFamily
+ , fontStyle
+ , fontStretch
+ , fontWeight
+ , fontSize
+ , origin
+ , xTranslation
+ , yTranslation
+ , rotation
+ , startTime
+ , durationTime
+ , outlineSize
+ , outlineColor
+ , fillColor
+ }
+ -> do
+ origin' <- originFromString origin
+ return
+ Gifcurry.TextOverlay
+ { Gifcurry.textOverlayText = text
+ , Gifcurry.textOverlayFontFamily = fontFamily
+ , Gifcurry.textOverlayFontStyle = fontStyle
+ , Gifcurry.textOverlayFontStretch = fontStretch
+ , Gifcurry.textOverlayFontWeight = fontWeight
+ , Gifcurry.textOverlayFontSize = fontSize
+ , Gifcurry.textOverlayOrigin = origin'
+ , Gifcurry.textOverlayXTranslation = xTranslation
+ , Gifcurry.textOverlayYTranslation = yTranslation
+ , Gifcurry.textOverlayRotation = rotation
+ , Gifcurry.textOverlayStartTime = startTime
+ , Gifcurry.textOverlayDurationTime = durationTime
+ , Gifcurry.textOverlayOutlineSize = outlineSize
+ , Gifcurry.textOverlayOutlineColor = outlineColor
+ , Gifcurry.textOverlayFillColor = fillColor
+ }
+ )
+ textOverlays
+ where
+ originFromString :: String -> IO Gifcurry.TextOverlayOrigin
+ originFromString origin' = do
+ let maybeOrigin = Gifcurry.textOverlayOriginFromString origin'
+ case maybeOrigin of
+ Nothing -> do
+ putStrLn $
+ "[WARNING] Origin " ++ origin' ++ " not valid! Defaulting to Center."
+ return Gifcurry.TextOverlayOriginCenter
+ Just origin'' -> return origin''
+
makeGifParams :: CliArgs -> Gifcurry.GifParams
makeGifParams
CliArgs
@@ -148,10 +298,7 @@ makeGifParams
, start_time
, duration_time
, width_size
- , quality_percent
- , font_choice
- , top_text
- , bottom_text
+ , quality
, left_crop
, right_crop
, top_crop
@@ -165,10 +312,9 @@ makeGifParams
, Gifcurry.startTime = start_time
, Gifcurry.durationTime = duration_time
, Gifcurry.widthSize = width_size
- , Gifcurry.qualityPercent = quality_percent
- , Gifcurry.fontChoice = font_choice
- , Gifcurry.topText = top_text
- , Gifcurry.bottomText = bottom_text
+ , Gifcurry.quality = fromMaybe Gifcurry.QualityMedium $
+ Gifcurry.qualityFromString quality
+ , Gifcurry.textOverlays = []
, Gifcurry.leftCrop = left_crop
, Gifcurry.rightCrop = right_crop
, Gifcurry.topCrop = top_crop
@@ -179,36 +325,17 @@ logo :: String
logo =
unlines
[ ""
- , " ppDPPPDbDDpp "
- , " pDPPPP )DPDp ) "
- , " PPPPP )pp DPPp ppppp PPP pDbDD "
- , " p )PPP PPPD PPPD pDPDPPPDP PPP "
- , " bP DPP pPPP )PPPb (PPP PPP )PPPPPP pDPPPDb PPP PPb PPbpDPP PPbpPP ·DPb pPD "
- , " (PPb )D (PPD bPPP PPP DDDDD PPP PPP PPb PPP PPb PPPP PPPP (PP pPPC "
- , " (PPPp PPP b )PPP DPPp PPP PPP PPP (PPb PPP PPb PPP PPP DPb PPP "
- , " PPPb DPPP pPp DPb DPDp PPP PPP PPP DPPp p PPP pPPb PPP PPP PPpPP "
- , " )PPPp (DPPP )PPb b (PPDDPPP PPP PPP (PDDDPC PDDP PPC PPP PPP )DPPP "
- , " )DPPp )DD DPPPb pbPP "
- , " )DPbp (PPPPPb PPC "
- , " SPDbDppppPPDPC "
- , ""
- ]
-
-icon :: String
-icon =
- unlines
- [ ""
- , " ppDPPPDbDDpp "
- , " pDPPPP )DPDp "
- , " PPPPP )pp DPPp "
- , " p )PPP PPPD PPPD "
- , " bP DPP pPPP )PPPb "
- , " (PPb )D (PPD bPPP "
- , " (PPPp PPP b )PPP "
- , " PPPb DPPP pPp DPb "
- , " )PPPp (DPPP )PPb b "
- , " )DPPp )DD DPPPb "
- , " )DPbp (PPPPPb "
- , " SPDbDppppPPDPC "
+ , " ▄▄▄▄▄▄▄▄ "
+ , " ▄▄████ ▀▀███▄ "
+ , " ████▀ ▄ ▀███ ▄ ▐██▌ ▄███▄ "
+ , " ▄ ▐███ ████ ▀███ ▄███▀▀██ ███ "
+ , " ▐█▌ ██ ▐███ ████ ███ ▐██ █████▌ ▄█████ ▐██▌ ██▌ ██▄██▌ ██▄██▌ ██▌ ███ "
+ , " ███ ▐▌ ███ ▐███▌ ███ ████▌ ▐██ ██▌ ███ ▐██▌ ██▌ ███▀ ███▀ ▐██ ███ "
+ , " ████ ███▀ ▐█ ███▌ ███ ██▌ ▐██ ██▌ ███ ▐██▌ ██▌ ██▌ ██▌ ██▌▐██ "
+ , " ▐███▄ ▐██▌ ██ ██ ███▄▄▄██▌ ▐██ ██▌ ███▄▄█ ███▄███▌ ██▌ ██▌ ████▌ "
+ , " ▀███ ▀███ ▐███ ▀ ▀▀▀▀▀ ▀▀ ▀▀ ▀▀▀ ▀▀▀ ▀▀ ▀▀ ███ "
+ , " ███▄ ▀ ████▌ ███▀ "
+ , " ▀███▄▄ █████▀ "
+ , " ▀▀▀▀▀▀▀ "
, ""
]
diff --git a/src/data/about-dialog-button-image.svg b/src/data/about-dialog-button-image.svg
index 6b34c7d..3e9ecff 100644
--- a/src/data/about-dialog-button-image.svg
+++ b/src/data/about-dialog-button-image.svg
@@ -5,49 +5,13 @@
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="30"
height="30"
viewBox="0 0 30 30"
version="1.1"
- id="svg8"
- sodipodi:docname="about-dialog-button-image.svg"
- inkscape:version="0.92.2 2405546, 2018-03-11">
-
+ id="svg8">
-
-
-
+
+
+ (C) 2018 David Lettier
+
+
@@ -79,28 +48,24 @@
+ style="display:inline;opacity:1;fill:#ffffff;fill-opacity:0;fill-rule:nonzero;stroke:none;stroke-width:0.1846476;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ id="path905" />
+ style="display:inline;fill:#6b86c3;fill-opacity:1;stroke:none;stroke-width:0.1846476;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1">
+ id="path884" />
+ style="display:inline;opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.1846476;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
diff --git a/src/data/check-icon.svg b/src/data/check-icon.svg
new file mode 100644
index 0000000..9ff85d8
--- /dev/null
+++ b/src/data/check-icon.svg
@@ -0,0 +1,53 @@
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/crop-icon.svg b/src/data/crop-icon.svg
new file mode 100644
index 0000000..77d2cc7
--- /dev/null
+++ b/src/data/crop-icon.svg
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/down-icon.svg b/src/data/down-icon.svg
new file mode 100644
index 0000000..897b331
--- /dev/null
+++ b/src/data/down-icon.svg
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/end-icon.svg b/src/data/end-icon.svg
new file mode 100644
index 0000000..226f331
--- /dev/null
+++ b/src/data/end-icon.svg
@@ -0,0 +1,51 @@
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
diff --git a/src/data/error-icon.svg b/src/data/error-icon.svg
new file mode 100644
index 0000000..1e31a6a
--- /dev/null
+++ b/src/data/error-icon.svg
@@ -0,0 +1,57 @@
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/file-icon.svg b/src/data/file-icon.svg
new file mode 100644
index 0000000..d5f7bc5
--- /dev/null
+++ b/src/data/file-icon.svg
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/gifcurry-icon.svg b/src/data/gifcurry-icon.svg
index 6aaa2ce..1174f57 100644
--- a/src/data/gifcurry-icon.svg
+++ b/src/data/gifcurry-icon.svg
@@ -5,58 +5,34 @@
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- id="svg8"
+ width="96"
+ height="96"
+ viewBox="0 0 25.4 25.4"
version="1.1"
- viewBox="0 0 55.240221 55.272122"
- height="55.272121mm"
- width="55.240219mm"
- sodipodi:docname="gifcurry-icon.svg"
- inkscape:version="0.92.2 2405546, 2018-03-11">
-
+ id="svg8">
+ x="-0.038469434"
+ width="1.0769389"
+ y="-0.1362645"
+ height="1.272529">
+ stdDeviation="2.0172845"
+ id="feGaussianBlur1017" />
+ x="-0.060019407"
+ width="1.1200387"
+ y="-0.059980609"
+ height="1.1199611">
+ stdDeviation="1.2333967"
+ id="feGaussianBlur1025" />
+
+
+ (C) 2018 David Lettier
+
+
+ transform="translate(-0.29107212,-26.564588)">
+ style="opacity:1;fill:#ffffff;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.33525831;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ id="path905" />
+ style="display:inline;fill:#491ea4;fill-opacity:1">
+ d="m 131.80384,-60.7582 c 1.64062,0.926881 1.64062,0.926881 1.64062,0.926881 l -0.73839,1.519334 z"
+ style="opacity:1;vector-effect:none;fill:#491ea4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.32291663;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1107.35998535;stroke-opacity:1" />
+ d="m 152.52545,-71.413259 c -2.28182,0.07369 -4.53962,0.489887 -6.69774,1.23465 -2.90794,1.012807 -5.58042,2.60451 -7.85604,4.678975 2.18762,4.189891 3.73999,9.369631 5.56046,19.461365 l 6.06777,-23.330445 5.34427,1.272355 c 7.46157,1.4718 10.72528,4.576889 12.74071,13.179301 2.07299,8.749211 3.56068,14.005444 5.18581,17.830118 3.3959,-5.752178 4.09672,-12.704761 1.91712,-19.018951 -3.26933,-9.426039 -12.29062,-15.628998 -22.26236,-15.307368 z m 15.80037,39.845734 c -2.29675,-4.260099 -3.89082,-9.440793 -5.76758,-19.844598 l -6.06778,23.330445 -5.34426,-1.272354 c -7.46157,-1.471803 -10.72529,-4.576891 -12.74071,-13.179302 -1.97752,-8.346329 -3.42143,-13.518573 -4.96103,-17.297985 -3.20735,5.664052 -3.83923,12.429395 -1.73624,18.589428 4.0806,11.889042 17.01815,18.227979 28.91381,14.166735 l 0.13984,-0.0482 c 2.78539,-0.977447 5.35431,-2.486807 7.56395,-4.444166 z m -13.62457,-31.59119 -7.14112,22.247082 h -2.56332 c 1.1663,3.768048 2.60934,5.610032 6.39232,6.619957 l 7.1411,-22.247084 h 2.56332 c -1.16629,-3.768047 -2.60933,-5.610032 -6.3923,-6.619955 z"
+ style="opacity:1;fill:#491ea4;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.53782058;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
+ d="m 142.72669,-45.823194 0.80544,-0.215075 -0.32748,-2.010777 z"
+ style="opacity:1;vector-effect:none;fill:#491ea4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.32291663;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1107.35998535;stroke-opacity:1" />
+ d="m 144.31264,-40.272112 0.68417,-0.639521 0.40617,0.860469 z"
+ style="opacity:1;vector-effect:none;fill:#491ea4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.32291663;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1107.35998535;stroke-opacity:1" />
+ d="m 161.09355,-56.53876 0.64851,-0.654283 -0.94262,-0.08335 z"
+ style="opacity:1;vector-effect:none;fill:#491ea4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.32291663;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1107.35998535;stroke-opacity:1" />
+ d="m 162.55824,-51.412123 0.76977,-0.229837 -0.46564,2.228656 z"
+ style="opacity:1;vector-effect:none;fill:#491ea4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.32291663;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1107.35998535;stroke-opacity:1" />
+ d="m 172.87069,-37.086941 1.62357,0.956177 -0.93444,-2.710169 z"
+ style="opacity:1;vector-effect:none;fill:#491ea4;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.32291663;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1107.35998535;stroke-opacity:1" />
+ d="m 12.628343,27.910573 c -1.136372,0.0367 -2.260783,0.243968 -3.335552,0.614868 -1.448184,0.504389 -2.7791083,1.297075 -3.9123951,2.330182 1.0894645,2.086612 1.86256,4.666179 2.7691738,9.691974 l 3.0218193,-11.61882 2.661503,0.633647 c 3.715945,0.732974 5.341308,2.279342 6.345009,6.563438 1.032373,4.357204 1.773264,6.974866 2.582597,8.879596 1.691194,-2.864648 2.040209,-6.327111 0.954746,-9.471647 -1.62816,-4.694272 -6.12087,-7.783415 -11.086901,-7.623238 z m 7.86876,19.843615 c -1.14381,-2.121576 -1.937675,-4.70162 -2.872323,-9.882829 L 14.602962,49.490178 11.941458,48.856533 C 8.2255182,48.123558 6.6001499,46.577189 5.5964445,42.293094 4.6116146,38.136529 3.8925336,35.560695 3.125799,33.678506 c -1.597294,2.820761 -1.9119797,6.189976 -0.864668,9.257741 2.032183,5.920874 8.475226,9.077735 14.399397,7.05519 l 0.06964,-0.02401 c 1.387155,-0.486779 2.666508,-1.238457 3.766937,-2.213243 z M 13.711911,32.021426 10.155559,43.100718 H 8.8789992 c 0.5808299,1.87653 1.2994778,2.793859 3.1834388,3.296812 l 3.556353,-11.079294 h 1.27656 c -0.580826,-1.876528 -1.299479,-2.793857 -3.18344,-3.29681 z"
+ style="display:inline;opacity:1;fill:#5d22da;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.26784056;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke" />
diff --git a/src/data/gifcurry-logo.svg b/src/data/gifcurry-logo.svg
index da83d20..525e5eb 100644
--- a/src/data/gifcurry-logo.svg
+++ b/src/data/gifcurry-logo.svg
@@ -5,72 +5,34 @@
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
- xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
- xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
- width="201.61172mm"
- height="65.272118mm"
- viewBox="0 0 201.61173 65.272118"
- version="1.1"
id="svg8"
- sodipodi:docname="gifcurry-logo.svg"
- inkscape:version="0.92.2 2405546, 2018-03-11">
-
+ version="1.1"
+ viewBox="0 0 201.61173 65.272118"
+ height="65.272118mm"
+ width="201.61172mm">
+ width="1.0769389"
+ x="-0.038469434"
+ id="filter1015"
+ style="color-interpolation-filters:sRGB">
+ id="feGaussianBlur1017"
+ stdDeviation="2.0172845" />
+ width="1.1200387"
+ x="-0.060019407"
+ id="filter1023"
+ style="color-interpolation-filters:sRGB">
+ id="feGaussianBlur1025"
+ stdDeviation="1.2333967" />
image/svg+xml
-
+
+
+
+ (C) 2018 David Lettier
+
+
+ style="display:inline;fill:#2f4474;fill-opacity:1;filter:url(#filter1015)">
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#2f4474;fill-opacity:1;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path961" />
-
+ id="path963" />
+ id="path965" />
+
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#2f4474;fill-opacity:1;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path969" />
+ id="path971" />
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#2f4474;fill-opacity:1;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ id="path973" />
+ id="path975" />
+ id="g1094">
+ d="m 27.11518,-0.334454 c -2.47074,0.07979 -4.91547,0.530445 -7.25227,1.336869 -3.14869,1.09666 -6.04243,2.820143 -8.50646,5.066358 2.36875,4.536781 4.04964,10.145363 6.02083,21.072615 l 6.57014,-25.262025 5.78673,1.377696 c 8.07933,1.593654 11.61325,4.95582 13.79553,14.270445 2.24462,9.473578 3.85549,15.164987 5.61517,19.306314 C 52.8219,30.605405 53.58074,23.077202 51.22069,16.240245 47.68069,6.033803 37.91249,-0.682714 27.11518,-0.334454 Z M 44.2237,42.810198 C 41.73679,38.197396 40.01074,32.58778 37.9786,21.322621 L 31.40846,46.584646 25.62173,45.206952 C 17.54241,43.613295 14.00848,40.251129 11.82619,30.936505 9.68494,21.899164 8.12149,16.298699 6.45443,12.206381 2.98154,18.339372 2.29734,25.664833 4.57444,32.33487 8.99288,45.208233 23.00156,52.071985 35.88209,47.674501 l 0.15141,-0.05219 c 3.016,-1.058373 5.79761,-2.692696 8.1902,-4.812109 z M 29.47111,8.603502 21.73877,32.69247 h -2.77554 c 1.26286,4.080013 2.82537,6.0745 6.92154,7.168039 l 7.73234,-24.088971 h 2.77554 C 35.1298,11.691526 33.56728,9.697038 29.47111,8.603502 Z"
+ style="display:inline;opacity:1;fill:#2f4474;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:0.58234793;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1;paint-order:fill markers stroke;filter:url(#filter1023)" />
+ transform="translate(-125.34941,72.964581)">
+ id="path884" />
+ id="path878" />
+ id="path886" />
+ id="path888" />
+ id="path890" />
+ id="path892" />
+ id="path894" />
+ id="path851" />
+ id="path1063" />
+ id="path1067" />
+ id="path1065" />
+ id="path1069" />
+ id="path1031" />
+ id="path1033" />
+ id="path1035" />
+ id="path1037" />
+ d="m 175.8193,50.5131 -0.65036,-3.696788 c 3.49141,-0.547672 4.58675,-1.471869 5.57941,-4.073312 h -1.91685 l -5.51095,-17.525514 5.3398,-1.232262 3.32027,14.616004 c 1.30071,-7.87279 2.39606,-11.090364 4.58675,-14.616004 l 5.20288,1.711476 c -2.43029,3.286034 -3.55987,6.708986 -5.71633,15.848266 -1.47186,6.36669 -4.34714,8.112396 -10.23462,8.968134 z" />
+ d="m 77.281721,18.440243 c -6.36669,0 -11.364199,4.381379 -11.364199,12.425315 0,8.112396 3.628328,12.459544 10.884986,12.459544 2.977968,0 6.058625,-0.924197 8.52315,-2.498754 V 29.085624 h -9.070822 l 0.547672,3.970624 h 3.080656 v 5.374034 c -0.992655,0.547672 -2.05377,0.787279 -3.217574,0.787279 -3.286033,0 -4.929051,-2.12223 -4.929051,-8.352003 0,-5.887477 2.464526,-8.352002 5.784789,-8.352002 1.916853,0 3.217575,0.581902 4.826362,1.814164 l 2.84105,-2.977967 c -1.951082,-1.711476 -4.449837,-2.90951 -7.907019,-2.90951 z" />
+ d="m 91.642074,14.983062 c -1.882624,0 -3.183345,1.334951 -3.183345,3.080657 0,1.745706 1.300721,3.080656 3.183345,3.080656 1.882624,0 3.217574,-1.33495 3.217574,-3.080656 0,-1.745706 -1.33495,-3.080657 -3.217574,-3.080657 z m 2.738361,9.584265 h -5.408264 v 18.175874 h 5.408264 z" />
+ d="m 106.917,20.76785 c 0.78728,0 1.81416,0.171148 2.84105,0.684591 l 1.47187,-3.52564 c -1.33495,-0.684591 -3.08066,-1.163804 -4.96328,-1.163804 -4.65522,0 -6.948596,2.704132 -6.948596,6.195543 v 1.608787 h -2.738361 v 3.765247 h 2.738361 v 14.410627 h 5.408266 V 28.332574 h 3.69679 l 0.5819,-3.765247 h -4.27869 v -1.40341 c 0,-1.711476 0.61613,-2.396067 2.19069,-2.396067 z" />
+ d="m 118.30527,23.985426 c -5.4425,0 -8.89968,4.039082 -8.89968,9.823871 0,5.750559 3.42295,9.515805 9.00236,9.515805 2.49876,0 4.44984,-0.821507 6.12709,-2.156459 L 122.1732,37.81415 c -1.30072,0.821509 -2.19069,1.232263 -3.49141,1.232263 -2.15646,0 -3.5941,-1.232263 -3.5941,-5.271346 0,-4.073312 1.33495,-5.64787 3.66256,-5.64787 1.23226,0 2.29338,0.410754 3.45718,1.300721 l 2.32761,-3.217574 c -1.74571,-1.471869 -3.69679,-2.224918 -6.22977,-2.224918 z" />
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#6b86c3;fill-opacity:1;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 141.81239,24.567327 h -5.40827 v 12.630692 c -0.78728,1.369181 -1.81416,2.12223 -2.94374,2.12223 -1.12957,0 -1.74571,-0.547672 -1.74571,-2.396066 V 24.567327 h -5.40825 v 13.041446 c 0,3.525641 1.67724,5.716329 5.06597,5.716329 2.39606,0 4.176,-0.958425 5.51095,-2.875279 l 0.2396,2.293378 h 4.68945 z" />
+ d="m 155.92404,24.053884 c -2.088,0 -3.86793,1.506099 -4.68944,4.039083 l -0.44499,-3.52564 h -4.72367 v 18.175874 h 5.40827 v -9.002364 c 0.5819,-2.772591 1.50609,-4.449837 3.76524,-4.449837 0.61613,0 1.06111,0.102689 1.64301,0.239607 l 0.85575,-5.237116 c -0.5819,-0.171147 -1.12958,-0.239607 -1.81417,-0.239607 z" />
+ style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:35.27777863px;line-height:125%;font-family:'Fira Sans';-inkscape-font-specification:'Fira Sans Bold';font-variant-ligatures:normal;font-variant-caps:normal;font-variant-numeric:normal;font-feature-settings:normal;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#6b86c3;fill-opacity:1;stroke:none;stroke-width:0.25672138px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
+ d="m 169.62922,24.053884 c -2.088,0 -3.86794,1.506099 -4.68945,4.039083 l -0.44498,-3.52564 h -4.72367 v 18.175874 h 5.40826 v -9.002364 c 0.5819,-2.772591 1.5061,-4.449837 3.76525,-4.449837 0.61613,0 1.06112,0.102689 1.64302,0.239607 l 0.85573,-5.237116 c -0.5819,-0.171147 -1.12956,-0.239607 -1.81416,-0.239607 z" />
+ id="path1029" />
+ id="path1039" />
+ id="path1041" />
+ id="path1043" />
+ id="path1053" />
+ id="path1045" />
+ id="path1047" />
+ id="path1049" />
+ id="path1051" />
+ id="path1055" />
+ id="path1057" />
+ id="path1059" />
+ id="path1061" />
+ d="m 80.923768,24.069851 1.42392,0.257869 -1.09853,-1.177253 z"
+ style="display:inline;opacity:1;vector-effect:none;fill:#6b86c3;fill-opacity:1;fill-rule:evenodd;stroke:none;stroke-width:1.32291663;stroke-linecap:butt;stroke-linejoin:round;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:1107.35998535;stroke-opacity:1" />
+ id="path875" />
+ id="g1141">
+ id="path4934" />
+ id="path4944" />
+ id="path4946" />
+ id="path4948" />
+ id="path4950" />
+ id="path4952" />
+ id="path4954" />
+ id="path4942" />
diff --git a/src/data/gray-pattern.png b/src/data/gray-pattern.png
new file mode 100644
index 0000000..70f8ef9
Binary files /dev/null and b/src/data/gray-pattern.png differ
diff --git a/src/data/green-pattern.png b/src/data/green-pattern.png
new file mode 100644
index 0000000..ab67d1c
Binary files /dev/null and b/src/data/green-pattern.png differ
diff --git a/src/data/gui.glade b/src/data/gui.glade
index 9d7552c..0103390 100644
--- a/src/data/gui.glade
+++ b/src/data/gui.glade
@@ -43,19 +43,48 @@ Author: David Lettier
about-dialog-button-image.svg
3
-
- 100
- 1
- 10
+
+ True
+ False
+ 5
+ plus-icon.svg
- 100
- 1
+ 0.98999999999999999
+ 0.01
+
+
+ True
+ False
+ 5
+ x-icon.svg
+
+
+ No
+ True
+ True
+ True
+ confirm-message-dialog-no-button-image
+ True
+
+
+ True
+ False
+ 5
+ check-icon.svg
+
+
+ Yes
+ True
+ True
+ True
+ confirm-message-dialog-yes-button-image
+ True
True
False
- gtk-cut
+ crop-icon.svg
1
@@ -63,32 +92,28 @@ Author: David Lettier
True
False
- gtk-dnd-multiple
+ save-as-gif-icon.svg
True
True
False
- True
- True
- vertical
-
+
True
- True
False
True
True
False
+ center
+ center
True
- True
False
This is the first frame of the GIF.
- True
gtk-missing-image
0
@@ -99,8 +124,8 @@ Author: David Lettier
True
+ True
False
- True
1
@@ -108,22 +133,8 @@ Author: David Lettier
- True
- True
- 0
-
-
-
-
- True
- False
- False
- vertical
-
-
- False
- True
- 1
+ 0
+ 0
@@ -131,13 +142,13 @@ Author: David Lettier
True
True
False
+ center
+ center
True
- True
False
This is the last frame of the GIF.
- True
gtk-missing-image
0
@@ -148,8 +159,8 @@ Author: David Lettier
True
+ True
False
- True
1
@@ -157,83 +168,93 @@ Author: David Lettier
- True
- True
- 2
+ 1
+ 0
-
-
- False
- True
- 1
-
-
-
-
- True
- False
- True
-
- First Frame
+
True
- False
False
- False
- Last Frame
- 0.98999999999999999
True
- True
-
-
- True
- True
- 0
-
-
-
-
- Last Frame
- True
- False
- False
- False
- False
- First Frame
- True
- True
+ True
+
+
+ First Frame
+ first-frame-preview-label-button
+ True
+ False
+ False
+ False
+ Last Frame
+ 0.99999999977648257
+ True
+
+
+ False
+ True
+ 0
+
+
+
+
+ Last Frame
+ last-frame-preview-label-button
+ True
+ False
+ False
+ False
+ False
+ First Frame
+ True
+
+
+ False
+ True
+ 1
+
+
- True
- True
- end
- 1
+ 0
+ 1
+ 2
True
True
- end
- 4
+ 0
+
+ True
+ False
+ 5
+ x-icon.svg
+
- gtk-cancel
+ Cancel
True
True
False
- True
+ in-file-chooser-dialog-cancel-button-image
True
+
+ True
+ False
+ 5
+ open-icon.svg
+
- gtk-open
+ Open
True
True
False
- True
+ in-file-chooser-dialog-open-button-image
True
@@ -242,77 +263,61 @@ Author: David Lettier
- 100
- 1
-
-
- gtk-no
- True
- True
- True
- True
- True
+ 0.98999999999999999
+ 0.01
True
False
5
5
- gtk-open
+ open-icon.svg
2
-
- 1
- 100
- 100
- 1
-
- 100
- 1
+ 0.98999999999999999
+ 0.01
True
False
5
5
- gtk-save
+ save-icon.svg
2
True
False
- gtk-save
+ file-icon.svg
1
-
+
True
False
- gtk-italic
+ text-icon.svg
- 100
- 1
+ 0.98999999999999999
+ 0.01
True
False
- gtk-sort-descending
+ upload-icon.svg
True
False
- gtk-media-record
+ save-as-video-icon.svg
True
True
False
This is a video preview of the GIF.
- True
- True
vertical
@@ -336,6 +341,7 @@ Author: David Lettier
True
+ True
False
@@ -347,6 +353,11 @@ Author: David Lettier
+
+ True
+ False
+ pause-icon.svg
+
1
3840
@@ -356,24 +367,21 @@ Author: David Lettier
True
False
- gtk-justify-fill
+ width-icon.svg
- 900
+ 1000
True
False
Gifcurry
mouse
- 1000
+ 1100
gifcurry-icon.svg
center
True
- True
False
- True
- True
vertical
@@ -384,7 +392,7 @@ Author: David Lettier
True
False
True
- Which video do you want to open?
+ Open which video?
True
@@ -394,8 +402,7 @@ Author: David Lettier
True
False
- 5
- gtk-open
+ open-icon.svg
False
@@ -407,6 +414,7 @@ Author: David Lettier
True
False
+ 5
Open
False
@@ -450,155 +458,35 @@ Author: David Lettier
-
- False
- Gifcurry - Click Yes or No
- center-on-parent
- True
- dialog-question
- dialog
- True
- center
- gifcurry-window
- gifcurry-window
- warning
- Are you sure you want to make a GIF that long?
-
-
- False
- vertical
- 2
-
-
- False
- True
- end
-
-
-
-
-
-
-
-
- False
- False
- 0
-
-
-
-
-
-
-
-
-
- gtk-yes
- True
- True
- True
- True
- True
-
diff --git a/src/data/info-icon.svg b/src/data/info-icon.svg
new file mode 100644
index 0000000..b27fb9f
--- /dev/null
+++ b/src/data/info-icon.svg
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
diff --git a/src/data/left-icon.svg b/src/data/left-icon.svg
new file mode 100644
index 0000000..54673f2
--- /dev/null
+++ b/src/data/left-icon.svg
@@ -0,0 +1,98 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/minus-icon.svg b/src/data/minus-icon.svg
new file mode 100644
index 0000000..60a8b5f
--- /dev/null
+++ b/src/data/minus-icon.svg
@@ -0,0 +1,88 @@
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
diff --git a/src/data/open-icon.svg b/src/data/open-icon.svg
new file mode 100644
index 0000000..e834a45
--- /dev/null
+++ b/src/data/open-icon.svg
@@ -0,0 +1,86 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/orange-pattern.png b/src/data/orange-pattern.png
new file mode 100644
index 0000000..533eec5
Binary files /dev/null and b/src/data/orange-pattern.png differ
diff --git a/src/data/pattern.svg b/src/data/pattern.svg
new file mode 100644
index 0000000..4259213
--- /dev/null
+++ b/src/data/pattern.svg
@@ -0,0 +1,189 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/pause-icon.svg b/src/data/pause-icon.svg
new file mode 100644
index 0000000..b219454
--- /dev/null
+++ b/src/data/pause-icon.svg
@@ -0,0 +1,78 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
diff --git a/src/data/pen-icon.svg b/src/data/pen-icon.svg
new file mode 100644
index 0000000..742f417
--- /dev/null
+++ b/src/data/pen-icon.svg
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+ A
+
+
+
+
+
+
diff --git a/src/data/plus-icon.svg b/src/data/plus-icon.svg
new file mode 100644
index 0000000..f105d0e
--- /dev/null
+++ b/src/data/plus-icon.svg
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/purple-pattern.png b/src/data/purple-pattern.png
new file mode 100644
index 0000000..b3f89b4
Binary files /dev/null and b/src/data/purple-pattern.png differ
diff --git a/src/data/right-icon.svg b/src/data/right-icon.svg
new file mode 100644
index 0000000..3d80337
--- /dev/null
+++ b/src/data/right-icon.svg
@@ -0,0 +1,90 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/save-as-gif-icon.svg b/src/data/save-as-gif-icon.svg
new file mode 100644
index 0000000..af5569e
--- /dev/null
+++ b/src/data/save-as-gif-icon.svg
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/save-as-video-icon.svg b/src/data/save-as-video-icon.svg
new file mode 100644
index 0000000..d486d59
--- /dev/null
+++ b/src/data/save-as-video-icon.svg
@@ -0,0 +1,156 @@
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/save-icon.svg b/src/data/save-icon.svg
new file mode 100644
index 0000000..d7fdd6b
--- /dev/null
+++ b/src/data/save-icon.svg
@@ -0,0 +1,109 @@
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/spiral-icon.svg b/src/data/spiral-icon.svg
new file mode 100644
index 0000000..d3ffafe
--- /dev/null
+++ b/src/data/spiral-icon.svg
@@ -0,0 +1,85 @@
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
diff --git a/src/data/start-icon.svg b/src/data/start-icon.svg
new file mode 100644
index 0000000..23d0784
--- /dev/null
+++ b/src/data/start-icon.svg
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
diff --git a/src/data/style-3-18.css b/src/data/style-3-18.css
new file mode 100644
index 0000000..736472e
--- /dev/null
+++ b/src/data/style-3-18.css
@@ -0,0 +1,54 @@
+/*
+ Gifcurry
+ (C) 2018 David Lettier
+ lettier.com
+*/
+
+@define-color _gifcurry_light_purple #6d4cff;
+@define-color _gifcurry_orange #ff6a28;
+
+* {
+ -GtkPaned-handle-size: 0;
+}
+
+button,
+GtkButton,
+colorswatch#add-color-button,
+GtkColorSwatch#add-color-button {
+ text-decoration: none;
+}
+
+button label,
+GtkButton GtkLabel {
+ text-decoration: none;
+}
+
+button:hover,
+GtkButton:hover,
+colorswatch#add-color-button:hover,
+GtkColorSwatch#add-color-button:hover {
+ text-decoration: none;
+}
+
+button label,
+GtkButton GtkLabel,
+button label:hover,
+GtkButton GtkLabel:hover {
+ text-decoration: none;
+}
+
+button.toggle:checked,
+button.radio:checked,
+GtkToggleButton:checked,
+GtkRadioButton:checked {
+ text-decoration: none;
+ background-color: @_gifcurry_light_purple;
+ text-shadow: none;
+}
+
+modelbutton check:checked,
+GtkModelButton .check:checked {
+ text-decoration: none;
+ background-color: @_gifcurry_orange;
+ text-shadow: none;
+}
diff --git a/src/data/style-3-20.css b/src/data/style-3-20.css
new file mode 100644
index 0000000..56ee899
--- /dev/null
+++ b/src/data/style-3-20.css
@@ -0,0 +1,51 @@
+/*
+ Gifcurry
+ (C) 2018 David Lettier
+ lettier.com
+*/
+
+button,
+colorswatch#add-color-button {
+ min-height: 25px;
+ -gtk-icon-shadow: none;
+}
+
+button label {
+ -gtk-icon-shadow: none;
+}
+
+button:hover,
+colorswatch#add-color-button:hover {
+ -gtk-icon-shadow: none;
+}
+
+button label,
+button label:hover {
+ -gtk-icon-shadow: none;
+}
+
+button.toggle:checked {
+ -gtk-icon-shadow: none;
+}
+
+entry {
+ min-height: 20px;
+}
+
+slider {
+ min-width: 10px;
+ min-height: 10px;
+}
+
+label:disabled {
+ color: #491bab;
+}
+
+button:disabled image {
+ background-color: inherit;
+ color: white;
+}
+
+modelbutton check {
+ -gtk-icon-source: none;
+}
diff --git a/src/data/style.css b/src/data/style.css
index 30ba914..34d76ef 100644
--- a/src/data/style.css
+++ b/src/data/style.css
@@ -4,9 +4,616 @@
lettier.com
*/
+@define-color _gifcurry_dark_purple #5d22da;
+@define-color _gifcurry_purple #6d28ff;
+@define-color _gifcurry_light_purple #6d4cff;
+@define-color _gifcurry_orange #ff6a28;
+@define-color _gifcurry_red #ff2878;
+
+window,
+GtkWindow {
+ background: @_gifcurry_dark_purple;
+ background-image: none;
+ border: none;
+ padding: 0px;
+ margin: 0px;
+ color: white;
+}
+
+window decoration,
+box,
+GtkBox,
+image,
+GtkImage,
+dialog label,
+GtkDialog GtkLabel {
+ background: none;
+ background-image: none;
+}
+
+box#text-overlays-box,
+GtkBox#text-overlays-box {
+ background: #491bab;
+}
+
+box#text-overlays-box > box,
+GtkBox#text-overlays-box > GtkBox {
+ background: @_gifcurry_dark_purple;
+}
+
+button,
+GtkButton,
+colorswatch#add-color-button,
+GtkColorSwatch#add-color-button,
+popover button,
+GtkPopover GtkButton {
+ background: @_gifcurry_purple;
+ border: 0px solid #521ec0;
+ padding: 0px;
+ margin: 2px;
+ padding: 2px;
+ color: white;
+ box-shadow: none;
+ text-shadow: none;
+ border-radius: 2px;
+}
+
+GtkButton {
+ padding-top: 5px;
+ padding-bottom: 5px;
+ border: 2px solid @_gifcurry_dark_purple;
+}
+
+button label,
+GtkButton GtkLabel {
+ color: white;
+ text-shadow: none;
+}
+
+button:hover,
+GtkButton:hover,
+colorswatch#add-color-button:hover,
+GtkColorSwatch#add-color-button:hover {
+ background-color: @_gifcurry_light_purple;
+ color: white;
+ border-radius: 0px;
+ text-shadow: none;
+}
+
+button label,
+GtkButton GtkLabel,
+button label:hover,
+GtkButton GtkLabel:hover {
+ text-shadow: none;
+}
+
+dialog buttonbox button,
+GtkDialog GtkButtonBox GtkButton,
+messagedialog buttonbox box,
+GtkMessageDialog GtkButtonBox GtkBox {
+ padding: 5px;
+}
+
+label:insensitive,
+GtkLabel:insensitive {
+ color: #491bab;
+}
+
+button:insensitive image,
+GtkButton:insensitive.image-button GtkImage {
+ background-color: inherit;
+ color: white;
+}
+
+button.toggle image,
+GtkToggleButton GtkImage,
+button.radio image,
+GtkRadioButton GtkImage,
+viewport image,
+GtkViewport GtkImage,
+viewport label,
+GtkViewport GtkLabel
+.sidebar-icon {
+ color: white;
+}
+
+viewport row,
+GtkViewport GtkPlacesViewRow,
+filechooserbutton arrow,
+combobox arrow,
+GtkFileChooserButton GtkImage {
+ background-color: inherit;
+ color: white;
+}
+
+viewport row:hover,
+GtkViewport GtkPlacesViewRow:hover {
+ background-color: @_gifcurry_light_purple;
+ color: white;
+}
+
+button#first-frame-preview-label-button label,
+GtkButton#first-frame-preview-label-button .label,
+button#last-frame-preview-label-button label,
+GtkButton#last-frame-preview-label-button .label {
+ background: none;
+ background-color: transparent;
+ color: white;
+}
+
+messagedialog buttonbox box.horizontal,
+GtkMessageDialog GtkButtonBox GtkBox.horizontal {
+ padding: 0px;
+ margin: 0px;
+}
+
+combobox window decoration,
+GtkComboBox GtkWindow {
+ background: @_gifcurry_purple;
+ border: none;
+ box-shadow: 0px 17px 5px -6px rgba(72,27,171,1);
+}
+
+spinbutton,
+GtkSpinButton {
+ background: @_gifcurry_dark_purple;
+ border: 0px solid #521ec0;
+ color: white;
+}
+
+spinbutton decoration {
+ border: none;
+ background-image: none;
+ border-image: none;
+ box-shadow: 0px 17px 5px -6px rgba(72,27,171,1);
+}
+
+spinbutton entry,
+GtkSpinButton GtkEntry {
+ border-radius: 0px;
+ margin: 1px;
+}
+
+spinbutton entry image,
+.spinbutton.entry GtkImage {
+ padding: 5px;
+}
+
+spinbutton progress,
+GtkEntry GtkSpinButton.entry {
+ background: @_gifcurry_orange;
+ border: none;
+ border-radius: 0px;
+ margin-top: 1px;
+ margin-bottom: 1px;
+ margin-left: 0px;
+ margin-right: 0px;
+}
+
+spinbutton button,
+GtkSpinButton GtkButton,
+button.image-button,
+GtkButton.image-button,
+button.emoji-section,
+GtkButton.emoji-section {
+ border-radius: 0px;
+}
+
+spinbutton image,
+GtkSpinButton GtkImage {
+ margin-left: 5px;
+}
+
+entry,
+GtkEntry {
+ background: #491bab;
+ border: 0px solid #521ec0;
+ box-shadow: none;
+ padding: 0px;
+ margin: 0px;
+ color: white;
+}
+
+GtkEntry {
+ padding: 5px;
+ border-radius: 0px;
+}
+
+GtkSpinButton.spinbutton {
+ background: @_gifcurry_purple;
+ border: 1px solid @_gifcurry_dark_purple;
+}
+
+GtkSpinButton.spinbutton:hover {
+ background-color: @_gifcurry_light_purple;
+}
+
+GtkSpinButton.entry {
+ background: #491bab;
+ border: none;
+}
+
+.spinbutton.entry {
+ padding: 10px;
+}
+
+entry:selected,
+GtkEntry:selected,
+entry:active,
+GtkEntry:active,
+entry:hover,
+GtkEntry:hover {
+ box-shadow: none;
+}
+
+GtkEntry:selected {
+ background-color: @_gifcurry_light_purple;
+}
+
+entry selection,
+.entry:selected {
+ background-color: @_gifcurry_light_purple;
+}
+
+entry#status-entry,
+GtkEntry#status-entry {
+ background: @_gifcurry_purple;
+ padding: 5px;
+}
+
+colorswatch,
+GtkColorSwatch,
+colorswatch overlay,
+GtkColorSwatch GtkOverlay {
+ border: none;
+ box-shadow: none;
+}
+
+separator,
+GtkSeparator {
+ background: @_gifcurry_dark_purple;
+ background-image: none;
+ border: none;
+ color: transparent;
+}
+
+popover,
+GtkPopover,
+entry decoration,
+GtkMenu.context-menu {
+ background: @_gifcurry_dark_purple;
+ color: white;
+ border: none;
+ box-shadow: 0px 17px 5px -6px rgba(72,27,171,1);
+}
+
+viewport,
+GtkViewport {
+ background-color: transparent;
+}
+
+popover viewport,
+GtkPopover GtkViewport,
+GtkMenu.context-menu GtkViewport,
+popover viewport label,
+GtkMenu.context-menu GtkViewport GtkLabel {
+ background: @_gifcurry_purple;
+ color: white;
+}
+
+popover entry,
+GtkPopover GtkEntry,
+GtkMenu.context-menu GtkEntry {
+ border-radius: 0px;
+ padding-left: 5px;
+ padding-right: 5px;
+ color: white;
+}
+
+tooltip,
+GtkTooltip,
+tooltip label,
+GtkTooltip GtkLabel {
+ background: @_gifcurry_dark_purple;
+ border: none;
+ padding: 0px;
+ margin: 0px;
+ color: white;
+}
+
+scrollbar,
+GtkScrollbar {
+ background: transparent;
+ border: none;
+}
+
+scrollbar contents,
+GtkScrollbar GtkContents {
+ background: transparent;
+ border: none;
+}
+
+scrollbar contents button,
+GtkScrollbar .arrow {
+ opacity: 0;
+ background: transparent;
+ color: transparent;
+}
+
+trough,
+.trough {
+ background: none;
+ background-image: none;
+ border-image: none;
+ border: none;
+}
+
+GtkFontChooser GtkScale.trough {
+ background: #491bab;
+}
+
+GtkFontChooser GtkScale.scale.highlight.left {
+ background-color: @_gifcurry_light_purple;
+}
+
+fontchooser highlight,
+GtkFontChooser GtkHighlight {
+ background: #491bab;
+}
+
+slider,
+.slider {
+ background-color: rgba(255, 255, 255, 0.8);
+ background-image: none;
+ border: none;
+ border-radius: 2px;
+ padding: 2.5px;
+ box-shadow: none;
+ margin: 0px;
+}
+
+fontchooser scale.horizontal slider,
+GtkFontChooser .slider {
+ background: white;
+ background-color: white;
+ background-image: none;
+ border-radius: 2px;
+}
+
+filechooser .frame,
+GtkFontChooser .frame,
+fontchooser .frame,
+GtkFontChooser .frame {
+ border: none;
+ border-image: none;
+ box-shadow: none;
+}
+
+menu,
+GtkTreeMenu {
+ background: @_gifcurry_dark_purple;
+ border: none;
+ box-shadow: none;
+}
+
+menu arrow,
+GtkTreeMenu .arrow {
+ background: transparent;
+ color: white;
+ border: none;
+}
+
+menuitem,
+GtkMenuItem,
+GtkAccelLabel,
+cellview,
+GtkCellView {
+ color: white;
+ background: transparent;
+ background-image: none;
+ box-shadow: none;
+ text-shadow: none;
+}
+
+GtkSeparatorMenuItem {
+ background: none;
+ background-image: none;
+ border: none;
+ color: transparent;
+}
+
+menuitem:hover,
+GtkMenuItem:hover,
+menuitem:active,
+GtkMenuItem:active,
+menuitem:selected,
+GtkMenuItem:selected {
+ background: @_gifcurry_light_purple;
+ box-shadow: none;
+ border: none;
+}
+
+stack,
+GtkStack {
+ background: @_gifcurry_dark_purple;
+ border: 0px solid #ddd;
+ padding: 0px;
+ margin: 0px;
+ color: white;
+}
+
+filechooserbutton decoration,
+GtkFileChooserButton GtkDecoration {
+ background: @_gifcurry_purple;
+ border: 1px solid #521ec0;
+ box-shadow: none;
+}
+
+dialog,
+GtkDialog,
+messagedialog,
+GtkMessageDialog {
+ background: @_gifcurry_dark_purple;
+ border: 0px solid #ddd;
+ padding: 0px;
+ margin: 0px;
+ color: white;
+}
+
+messagedialog button,
+GtkMessageDialog GtkButton {
+ padding: 5px;
+}
+
+filechooser entry.search,
+GtkFileChooser GtkEntry.search,
+fontchooser entry,
+GtkFileChooser GtkEntry {
+ border-radius: 1px;
+ padding-left: 5px;
+ padding-right: 5px;
+ border: none;
+ box-shadow: none;
+}
+
+placessidebar,
+GtkPlacesSidebar {
+ background: @_gifcurry_dark_purple;
+ border: 0px solid #ddd;
+ padding: 0px;
+ margin: 0px;
+ color: white;
+}
+
+list,
+.list {
+ box-shadow: inherit;
+ background-color: inherit;
+}
+
+listview,
+GtkListView {
+ background: none;
+ background-color: transparent;
+}
+
+placesview list,
+GtkPlacesView GtkList {
+ background: @_gifcurry_dark_purple;
+ border: 0px solid #ddd;
+ padding: 0px;
+ margin: 0px;
+ color: white;
+}
+
+filechooser row.sidebar-row:active,
+GtkFileChooser GtkSidebarRow.sidebar-row:active,
+filechooser row.sidebar-row:hover,
+GtkFileChooser GtkSidebarRow.sidebar-row:hover,
+filechooser row.sidebar-row:selected,
+GtkFileChooser GtkSidebarRow.sidebar-row:selected {
+ background-color: @_gifcurry_light_purple;
+ color: white;
+}
+
+GtkPathBar GtkImage {
+ padding: 3px;
+}
+
+GtkPlacesViewRow GtkLabel,
+GtkDialog GtkViewport GtkLabel {
+ color: white;
+}
+
+treeview,
+GtkTreeView {
+ background: @_gifcurry_dark_purple;
+ border: 0px solid #ddd;
+ padding: 0px;
+ margin: 0px;
+ color: white;
+}
+
+treeview:selected,
+GtkTreeView:selected {
+ background-color: @_gifcurry_light_purple;
+ color: white;
+}
+
+revealer,
+GtkRevealer,
+button image,
+GtkButton GtkImage {
+ color: white;
+}
+
+revealer label,
+GtkRevealer GtkLabel {
+ color: white;
+}
+
+revealer box#pathbarbox,
+GtkRevealer GtkBox#pathbarbox {
+ border: none;
+ box-shadow: none;
+ background: none;
+}
+
+dialog popover box,
+GtkDialog GtkPopover GtkBox,
+dialog popover label,
+GtkDialog GtkPopover GtkLabel {
+ background-color: transparent;
+}
+
+modelbutton,
+GtkModelButton {
+ background: @_gifcurry_dark_purple;
+ background-image: none;
+ border-image: none;
+ color: white;
+}
+
+modelbutton:hover,
+GtkModelButton:hover {
+ background: @_gifcurry_light_purple;
+ background-image: none;
+ border-image: none;
+ color: white;
+}
+
+modelbutton label,
+GtkModelButton GtkLabel {
+ color: white;
+}
+
+modelbutton check,
+GtkModelButton .check {
+ background: @_gifcurry_dark_purple;
+ background-image: none;
+ border-image: none;
+ border: none;
+ box-shadow: none;
+ border-radius: 2px;
+ color: white;
+ animation-name: none;
+ padding: 2px;
+}
+
+modelbutton check:hover,
+modelbutton check:hover,
+GtkModelButton .check:hover {
+ background-image: none;
+ border-image: none;
+ border: none;
+ box-shadow: none;
+ border-radius: 2px;
+ color: white;
+}
+
dialog#about-dialog,
GtkDialog#about-dialog {
- background: #5d22da;
+ background: @_gifcurry_dark_purple;
border: none;
padding: 0px;
margin: 0px;
@@ -42,45 +649,54 @@ GtkDialog#about-dialog GtkTrough {
}
dialog#about-dialog scrollbar slider,
-GtkDialog#about-dialog GtkScrollbar GtkSlider {
+GtkDialog#about-dialog GtkScrollbar .slider {
background-color: white;
border: none;
}
dialog#about-dialog button,
GtkDialog#about-dialog GtkButton {
- background-color: whitesmoke;
+ background: @_gifcurry_purple;
background-image: none;
border: none;
border-radius: 2px;
text-shadow: none;
- color: #333
+ color: white
}
dialog#about-dialog button:hover,
GtkDialog#about-dialog GtkButton:hover {
- background-color: white;
+ background-color: @_gifcurry_light_purple;
+ color: white;
}
dialog#about-dialog button label,
GtkDialog#about-dialog GtkButton GtkLabel {
- color: #333;
+ color: white
+}
+
+dialog#about-dialog button:hover label,
+GtkDialog#about-dialog GtkButton:hover GtkLabel {
+ color: white;
}
.gifcurry-label-error {
- background-color: #eb3b5a;
+ background-color: @_gifcurry_red;
color: white;
font-size: 15px;
+ padding: 5px;
}
.gifcurry-label-warning {
- background-color: #fed330;
- color: #1e272e;
+ background: @_gifcurry_orange;
+ color: white;
font-size: 15px;
+ padding: 5px;
}
.gifcurry-label-ok {
background-color: transparent;
color: inherit;
font-size: inherit;
+ padding: 0px;
}
diff --git a/src/data/text-icon.svg b/src/data/text-icon.svg
new file mode 100644
index 0000000..cf519f1
--- /dev/null
+++ b/src/data/text-icon.svg
@@ -0,0 +1,136 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+ A
+
+
+
diff --git a/src/data/up-icon.svg b/src/data/up-icon.svg
new file mode 100644
index 0000000..42e1201
--- /dev/null
+++ b/src/data/up-icon.svg
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/upload-icon.svg b/src/data/upload-icon.svg
new file mode 100644
index 0000000..e0a06b1
--- /dev/null
+++ b/src/data/upload-icon.svg
@@ -0,0 +1,102 @@
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/warning-icon.svg b/src/data/warning-icon.svg
new file mode 100644
index 0000000..53d8889
--- /dev/null
+++ b/src/data/warning-icon.svg
@@ -0,0 +1,93 @@
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/width-icon.svg b/src/data/width-icon.svg
new file mode 100644
index 0000000..414a5bc
--- /dev/null
+++ b/src/data/width-icon.svg
@@ -0,0 +1,104 @@
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/data/x-icon.svg b/src/data/x-icon.svg
new file mode 100644
index 0000000..e35617d
--- /dev/null
+++ b/src/data/x-icon.svg
@@ -0,0 +1,96 @@
+
+
+
+
+
+
+
+
+
+
+
+ image/svg+xml
+
+
+
+
+ (C) 2018 David Lettier
+
+
+
+
+
+
+
+
+
+
diff --git a/src/gui/GuiCapabilities.hs b/src/gui/GuiCapabilities.hs
index 2c755ed..a27fb16 100644
--- a/src/gui/GuiCapabilities.hs
+++ b/src/gui/GuiCapabilities.hs
@@ -16,6 +16,7 @@ import Text.ParserCombinators.ReadP
import Data.Text
import qualified GI.Gtk
+import Paths_Gifcurry
import qualified GuiRecords as GR
import GuiStyle
import GuiMisc
@@ -24,7 +25,7 @@ checkCapabilitiesAndNotify :: GR.GuiComponents -> IO ()
checkCapabilitiesAndNotify
GR.GuiComponents
{ GR.inFileChooserButtonImage = inFileChooserButtonImage
- , GR.inFileChooserDialogLabel = inFileChooserDialogLabel
+ , GR.inFileChooserDialogLabel = inFileChooserDialogLabel
}
= do
ffmpegEncoders <- getFfmpegEncoders
@@ -53,57 +54,57 @@ checkCapabilitiesAndNotify
, hasGstDecoders'
] of
(False:_) -> do
- setImageToIcon "gtk-dialog-error"
+ setImageToFile "data/error-icon.svg"
setLabelText "FFmpeg not found. Cannot create GIFs."
setLabelStyle "gifcurry-label-error"
(_:False:_) -> do
- setImageToIcon "gtk-dialog-error"
+ setImageToFile "data/error-icon.svg"
setLabelText "FFprobe not found. Cannot create GIFs."
setLabelStyle "gifcurry-label-error"
(_:_:False:_) -> do
- setImageToIcon "gtk-dialog-error"
+ setImageToFile "data/error-icon.svg"
setLabelText "ImageMagick convert not found. Cannot create GIFs."
setLabelStyle "gifcurry-label-error"
(_:_:_:False:_) -> do
- setImageToIcon "gtk-dialog-error"
+ setImageToFile "data/error-icon.svg"
setLabelText "ImageMagick identify not found. Cannot create GIFs."
setLabelStyle "gifcurry-label-error"
(_:_:_:_:False:_) -> do
- setImageToIcon "gtk-dialog-error"
+ setImageToFile "data/error-icon.svg"
setLabelText "FFmpeg version too old. Upgrade to at least version 3.4.2."
setLabelStyle "gifcurry-label-error"
(_:_:_:_:_:False:_) -> do
- setImageToIcon "gtk-dialog-error"
+ setImageToFile "data/error-icon.svg"
setLabelText "FFmpeg does not have the VP9 encoder. Cannot save the GIF as a video."
setLabelStyle "gifcurry-label-error"
(_:_:_:_:_:_:False:_) -> do
- setImageToIcon "gtk-dialog-error"
+ setImageToFile "data/error-icon.svg"
setLabelText "FFmpeg is missing decoders. Cannot make GIFs for some videos."
setLabelStyle "gifcurry-label-warning"
(_:_:_:_:_:_:_:False:_) -> do
- setImageToIcon "gtk-dialog-error"
- setLabelText "\"gtksink\" not found. No video preview. Install the GStreamer 1.0 bad plugins version 1.8 or higher."
+ setImageToFile "data/error-icon.svg"
+ setLabelText "\"gtksink\" not found. No video preview. Install all GStreamer 1.0 plugins."
setLabelStyle "gifcurry-label-warning"
(_:_:_:_:_:_:_:_:False:_) -> do
- setImageToIcon "gtk-dialog-warning"
+ setImageToFile "data/warning-icon.svg"
setLabelText "\"gst-libav\" not found. Video preview may not work."
setLabelStyle "gifcurry-label-warning"
(_:_:_:_:_:_:_:_:_:False:_) -> do
- setImageToIcon "gtk-dialog-warning"
+ setImageToFile "data/warning-icon.svg"
setLabelText "GStreamer is missing decoders. Video preview may not work."
setLabelStyle "gifcurry-label-warning"
_ -> do
GI.Gtk.widgetHide inFileChooserDialogLabel
- setImageToIcon "gtk-open"
+ setImageToFile "data/open-icon.svg"
setLabelText ""
setLabelStyle "gifcurry-label-ok"
where
- setImageToIcon :: Text -> IO ()
- setImageToIcon iconName =
- GI.Gtk.imageSetFromIconName
- inFileChooserButtonImage
- (Just iconName)
- (enumToInt32 GI.Gtk.IconSizeButton)
+ setImageToFile :: String -> IO ()
+ setImageToFile iconFilePathName = do
+ filePathName <- getDataFileName iconFilePathName
+ GI.Gtk.imageSetFromFile
+ inFileChooserButtonImage
+ (Just filePathName)
setLabelText :: Text -> IO ()
setLabelText =
GI.Gtk.labelSetText
diff --git a/src/gui/GuiMisc.hs b/src/gui/GuiMisc.hs
index 9d23155..b02e760 100644
--- a/src/gui/GuiMisc.hs
+++ b/src/gui/GuiMisc.hs
@@ -17,20 +17,36 @@ import Data.Int
import Data.Maybe
import Data.Char
import Data.Text
+import Data.List
import qualified GI.Gtk
enumToInt32 :: (Enum a, Ord a) => a -> Int32
-enumToInt32 enum = fromIntegral (fromEnum enum) :: Int32
+enumToInt32 = fromIntegral . fromEnum
floatToInt32 :: Float -> Int32
floatToInt32 f = enumToInt32 (round f :: Int)
+floatToDouble :: Float -> Double
+floatToDouble = realToFrac
+
+doubleToFloat :: Double -> Float
+doubleToFloat = realToFrac
+
+doubleToInt :: Double -> Int
+doubleToInt = truncate
+
int32ToDouble :: Int32 -> Double
int32ToDouble = fromIntegral
int32ToFloat :: Int32 -> Float
int32ToFloat = fromIntegral
+int32ToInt :: Int32 -> Int
+int32ToInt = fromIntegral
+
+int64ToDouble :: Int64 -> Double
+int64ToDouble = fromIntegral
+
entryGetMaybeFloat :: GI.Gtk.Entry -> IO (Maybe Float)
entryGetMaybeFloat entry = do
text <- Data.Text.strip <$> GI.Gtk.entryGetText entry
@@ -70,10 +86,10 @@ safeDivide :: (Fractional a, Eq a) => a -> a -> Maybe a
safeDivide n d = if d == 0.0 then Nothing else Just $ n / d
clamp :: (Fractional a, Eq a, Ord a) => a -> a -> a -> a
-clamp min max v
- | v <= min = min
- | v >= max = max
- | otherwise = v
+clamp min' max' v
+ | v <= min' = min'
+ | v >= max' = max'
+ | otherwise = v
safeRunProcessGetOutput :: String -> [String] -> IO (System.Exit.ExitCode, String, String)
safeRunProcessGetOutput processName args =
@@ -98,3 +114,15 @@ hasText needle haystack =
Data.Text.isInfixOf needle $
Data.Text.toLower $
Data.Text.pack haystack
+
+listElementsEqual :: Eq a => [a] -> Bool
+listElementsEqual (x:xs) = Data.List.all (== x) xs
+listElementsEqual [] = False
+
+truncatePastDigit :: RealFrac a => a -> Int -> a
+truncatePastDigit frac num = fromIntegral int / trunc
+ where
+ int :: Int
+ int = floor (frac * trunc)
+ trunc :: Fractional b => b
+ trunc = 10.0^num
diff --git a/src/gui/GuiPreview.hs b/src/gui/GuiPreview.hs
index 3ab6569..cbd3a87 100644
--- a/src/gui/GuiPreview.hs
+++ b/src/gui/GuiPreview.hs
@@ -14,6 +14,10 @@ module GuiPreview where
import GHC.Float
import System.FilePath
import System.IO.Temp
+import Control.Monad
+import Control.Monad.IO.Class
+import Control.Concurrent
+import Text.Printf
import Data.Int
import Data.Maybe
import Data.Text
@@ -21,24 +25,33 @@ import Data.List
import Data.Bits
import Data.IORef
import Data.GI.Base.Properties
-import Control.Monad
-import Control.Concurrent
import qualified GI.GLib
+import qualified GI.Gdk
import qualified GI.Gtk
import qualified GI.Gst
import qualified GI.Cairo
import qualified GiCairoCairoBridge
import qualified Graphics.Rendering.Cairo as GRC
+import qualified Graphics.Rendering.Pango.Cairo as GRPC
+import qualified Graphics.Rendering.Pango.Layout as GRPL
+import Paths_Gifcurry
import qualified Gifcurry
( gif
, GifParams(..)
+ , Quality(QualityLow)
, defaultGifParams
)
import qualified GtkMainSyncAsync (gtkMainSync, gtkMainAsync)
import qualified GuiRecords as GR
+import qualified GuiTextOverlays
import GuiMisc
+data ForFramePreview =
+ ForFramePreviewFirst
+ | ForFramePreviewLast
+ | ForFramePreviewNone
+
blankPreviewIcon :: String
blankPreviewIcon = "gtk-discard"
@@ -48,72 +61,66 @@ framePreviewDirectoryName = "gifcurry-frame-previews"
buildVideoPreviewWidgetAndPlaybinElement :: IO (Maybe GI.Gtk.Widget, Maybe GI.Gst.Element)
buildVideoPreviewWidgetAndPlaybinElement = do
maybeGtkSink <- GI.Gst.elementFactoryMake "gtksink" (Just "MultimediaPlayerGtkSink")
- maybeVideoPreviewWidget <-
- case maybeGtkSink of
- Nothing -> return Nothing
- Just gtkSink ->
- Data.GI.Base.Properties.getObjectPropertyObject gtkSink "widget" GI.Gtk.Widget
- maybePlaybinElement <- GI.Gst.elementFactoryMake "playbin" (Just "MultimediaPlayerPlaybin")
- case (maybeVideoPreviewWidget, maybePlaybinElement) of
- (Just videoPreviewWidget, Just playbinElement) -> do
- -- Turns off the subtitles.
- let flags = flip setBit 10 $ flip setBit 9 $ flip setBit 4 $ flip setBit 1 $ bit 0
- _ <- Data.GI.Base.Properties.setObjectPropertyObject playbinElement "video-sink" maybeGtkSink
- _ <- Data.GI.Base.Properties.setObjectPropertyBool playbinElement "force-aspect-ratio" True
- _ <- Data.GI.Base.Properties.setObjectPropertyDouble playbinElement "volume" 0.0
- _ <- Data.GI.Base.Properties.setObjectPropertyInt playbinElement "flags" flags
- _ <- GI.Gtk.widgetShow videoPreviewWidget
- return ()
- _ -> return ()
- return (maybeVideoPreviewWidget, maybePlaybinElement)
+ case maybeGtkSink of
+ Nothing -> return (Nothing, Nothing)
+ Just gtkSink -> do
+ maybeVideoPreviewWidget <- Data.GI.Base.Properties.getObjectPropertyObject gtkSink "widget" GI.Gtk.Widget
+ maybePlaybinElement <- GI.Gst.elementFactoryMake "playbin" (Just "MultimediaPlayerPlaybin")
+ case (maybeVideoPreviewWidget, maybePlaybinElement) of
+ (Just videoPreviewWidget, Just playbinElement) -> do
+ -- Turns off the subtitles.
+ let flags = flip setBit 10 $ flip setBit 9 $ flip setBit 4 $ flip setBit 1 $ bit 0
+ _ <- Data.GI.Base.Properties.setObjectPropertyObject playbinElement "video-sink" maybeGtkSink
+ _ <- Data.GI.Base.Properties.setObjectPropertyBool playbinElement "force-aspect-ratio" True
+ _ <- Data.GI.Base.Properties.setObjectPropertyDouble playbinElement "volume" 0.0
+ _ <- Data.GI.Base.Properties.setObjectPropertyInt playbinElement "flags" flags
+ _ <- GI.Gtk.widgetShow videoPreviewWidget
+ return ()
+ _ -> return ()
+ return (maybeVideoPreviewWidget, maybePlaybinElement)
runGuiPreview :: GR.GuiComponents -> IO ()
-runGuiPreview
- guiComponents@GR.GuiComponents
- { GR.maybeVideoPreviewWidget = (Just videoPreviewWidget)
- , GR.maybePlaybinElement = (Just _)
- , GR.mainPreviewBox
- , GR.videoPreviewBox
- , GR.videoPreviewOverlayChildBox
- , GR.videoPreviewDrawingArea
- }
- = do
- mainPreviewBoxChildCount <-
- Data.List.length <$> GI.Gtk.containerGetChildren mainPreviewBox
- when (mainPreviewBoxChildCount <= 0) $
- GI.Gtk.boxPackStart mainPreviewBox videoPreviewBox True True 0
- videoPreviewOverlayChildBoxChildCount <-
- Data.List.length <$> GI.Gtk.containerGetChildren videoPreviewOverlayChildBox
- when (videoPreviewOverlayChildBoxChildCount <= 0) $ do
- _ <- GI.Gtk.boxPackStart videoPreviewOverlayChildBox videoPreviewWidget True True 0
- _ <- GI.Gtk.onWidgetDraw
- videoPreviewDrawingArea
- (drawCropGrid guiComponents videoPreviewDrawingArea)
- return ()
- runPreviewLoopIfNotRunning
- guiComponents $
- preview guiComponents videoPreview
-runGuiPreview
- guiComponents@GR.GuiComponents
- { GR.mainPreviewBox
- , GR.imagesPreviewBox
- , GR.firstFramePreviewImageDrawingArea
- , GR.lastFramePreviewImageDrawingArea
- }
- = do
- childrenCount <- Data.List.length <$> GI.Gtk.containerGetChildren mainPreviewBox
- when (childrenCount <= 0) $ do
- _ <- GI.Gtk.boxPackStart mainPreviewBox imagesPreviewBox True True 0
- _ <- GI.Gtk.onWidgetDraw
- firstFramePreviewImageDrawingArea
- (drawCropGrid guiComponents firstFramePreviewImageDrawingArea)
- _ <- GI.Gtk.onWidgetDraw
- lastFramePreviewImageDrawingArea
- (drawCropGrid guiComponents lastFramePreviewImageDrawingArea)
- return ()
- runPreviewLoopIfNotRunning
- guiComponents $
- preview guiComponents firstAndLastFramePreview
+runGuiPreview guiComponents = do
+ setupVideoPreviewPauseToggleButton guiComponents
+ handlePreviewType guiComponents
+ runPreviewOverlay guiComponents
+ runTimeSlicesWidget guiComponents
+ where
+ handlePreviewType :: GR.GuiComponents -> IO ()
+ handlePreviewType
+ GR.GuiComponents
+ { GR.mainPreviewBox
+ , GR.maybeVideoPreviewWidget = (Just videoPreviewWidget)
+ , GR.maybePlaybinElement = (Just _)
+ , GR.videoPreviewBox
+ , GR.videoPreviewOverlayChildBox
+ }
+ = do
+ mainPreviewBoxChildCount <-
+ Data.List.length <$> GI.Gtk.containerGetChildren mainPreviewBox
+ when (mainPreviewBoxChildCount <= 0) $ do
+ GI.Gtk.boxPackStart mainPreviewBox videoPreviewBox True True 0
+ runPreviewLoopIfNotRunning
+ guiComponents $
+ preview guiComponents videoPreview
+ videoPreviewOverlayChildBoxChildCount <-
+ Data.List.length <$> GI.Gtk.containerGetChildren videoPreviewOverlayChildBox
+ when (videoPreviewOverlayChildBoxChildCount <= 0) $ do
+ _ <- GI.Gtk.boxPackStart videoPreviewOverlayChildBox videoPreviewWidget True True 0
+ return ()
+ handlePreviewType
+ GR.GuiComponents
+ { GR.mainPreviewBox
+ , GR.imagesPreviewBox
+ }
+ = do
+ childrenCount <- Data.List.length <$> GI.Gtk.containerGetChildren mainPreviewBox
+ when (childrenCount <= 0) $ do
+ _ <- GI.Gtk.boxPackStart mainPreviewBox imagesPreviewBox True True 0
+ runPreviewLoopIfNotRunning
+ guiComponents $
+ preview guiComponents firstAndLastFramePreview
+ return ()
runPreviewLoopIfNotRunning :: GR.GuiComponents -> IO Bool -> IO ()
runPreviewLoopIfNotRunning
@@ -131,8 +138,7 @@ runPreviewLoopIfNotRunning
where
getLoopRunning :: IO Bool
getLoopRunning =
- readIORef guiPreviewStateRef >>=
- \ x -> return $ GR.loopRunning x
+ GR.loopRunning <$> readIORef guiPreviewStateRef
preview
:: GR.GuiComponents
@@ -168,7 +174,7 @@ preview
let invalidDurationTime = durationTime <= 0.0
let invalidInVideoWidth = inVideoWidth <= 0.0
let invalidInVideoHeight = inVideoHeight <= 0.0
- let inputInvalid =
+ let inputInvalid =
invalidInFilePath
|| invalidStartTime
|| invalidDurationTime
@@ -186,13 +192,16 @@ preview
inVideoHeight
else
resetWindow guiComponents
- atomicWriteIORef guiPreviewStateRef
- guiPreviewState
- { GR.maybeInFilePath = if invalidInFilePath then Nothing else Just inFilePath
- , GR.maybeStartTime = if invalidStartTime then Nothing else Just startTime
- , GR.maybeDurationTime = if invalidDurationTime then Nothing else Just durationTime
- , GR.loopRunning = True
- }
+ atomicModifyIORef' guiPreviewStateRef $
+ \ guiPreviewState' ->
+ ( guiPreviewState'
+ { GR.maybeInFilePath = if invalidInFilePath then Nothing else Just inFilePath
+ , GR.maybeStartTime = if invalidStartTime then Nothing else Just startTime
+ , GR.maybeDurationTime = if invalidDurationTime then Nothing else Just durationTime
+ , GR.loopRunning = True
+ }
+ , ()
+ )
return True
videoPreview
@@ -205,9 +214,10 @@ videoPreview
-> Float
-> IO ()
videoPreview
- GR.GuiComponents
+ guiComponents@GR.GuiComponents
{ GR.window
, GR.cropToggleButton
+ , GR.textOverlaysToggleButton
, GR.mainPreviewBox
, GR.videoPreviewBox
, GR.maybePlaybinElement = (Just playbinElement)
@@ -225,47 +235,39 @@ videoPreview
GI.Gtk.widgetShow videoPreviewBox
sizePreviewAndWindow
handleChanges
- handleCropMode
+ updateVideoPreviewAspectRatio
where
sizePreviewAndWindow :: IO ()
sizePreviewAndWindow = do
- let widthRatio = inVideoWidth / inVideoHeight
- let heightRatio = inVideoHeight / inVideoWidth
- let previewWidth =
- if inVideoWidth >= inVideoHeight
- then desiredPreviewSize
- else desiredPreviewSize * widthRatio
- let previewHeight =
- if inVideoWidth < inVideoHeight
- then desiredPreviewSize
- else desiredPreviewSize * heightRatio
- let previewWidth' = floatToInt32 previewWidth
- let previewHeight' = floatToInt32 previewHeight
+ let (previewWidth, previewHeight) =
+ getPreviewWidthAndHeight inVideoWidth inVideoHeight
GI.Gtk.widgetSetSizeRequest
window
- previewWidth'
+ (floatToInt32 previewWidth)
(-1)
GI.Gtk.widgetSetSizeRequest
mainPreviewBox
- previewWidth'
- previewHeight'
+ (floatToInt32 previewWidth)
+ (floatToInt32 previewHeight)
resizeWindow window
+ return ()
handleChanges :: IO ()
handleChanges = do
- (couldQueryDuration, videoDuration) <-
- GI.Gst.elementQueryDuration playbinElement GI.Gst.FormatTime
- (couldQueryPosition, videoPosition) <-
- GI.Gst.elementQueryPosition playbinElement GI.Gst.FormatTime
- let endTime = startTime + durationTime
- let startTimeInNano = secondsToNanoseconds startTime
- let endTimeInNano = secondsToNanoseconds endTime
+ (playbinDuration, playbinPosition) <- getPlaybinDurationAndPosition guiComponents
+ let videoDuration = fromMaybe (-1) playbinDuration
+ let videoPosition = fromMaybe (-1) playbinPosition
+ let endTime = startTime + durationTime
+ let startTimeInNano = secondsToNanoseconds startTime
+ let endTimeInNano = secondsToNanoseconds endTime
let inFilePathChanged = fromMaybe "" maybeInFilePath /= inFilePath
let startTimeChanged = fromMaybe (-1.0) maybeStartTime /= startTime
- let startOver =
- (couldQueryDuration && couldQueryPosition && videoDuration > 0)
- && ( videoPosition >= videoDuration
- || videoPosition >= endTimeInNano
- || videoPosition < startTimeInNano
+ let nearTheEnd = (videoDuration - videoPosition) <= 500000
+ let startOver =
+ videoDuration > 0
+ && ( videoPosition >= videoDuration
+ || videoPosition >= endTimeInNano
+ || videoPosition < startTimeInNano
+ || nearTheEnd
)
let seekToStart = startTimeChanged || inFilePathChanged || startOver
when inFilePathChanged $ do
@@ -276,6 +278,7 @@ videoPreview
(Just $ pack $ "file://" ++ inFilePath)
_ <- Data.GI.Base.Properties.setObjectPropertyDouble playbinElement "volume" 0.0
resizeWindow window
+ return ()
when seekToStart $ do
_ <- GI.Gst.elementSetState playbinElement GI.Gst.StatePaused
_ <- GI.Gst.elementSeekSimple
@@ -283,17 +286,18 @@ videoPreview
GI.Gst.FormatTime
[GI.Gst.SeekFlagsFlush]
startTimeInNano
- _ <- GI.Gst.elementSetState playbinElement GI.Gst.StatePlaying
+ playPlaybinElement guiComponents
return ()
- handleCropMode :: IO ()
- handleCropMode = do
- cropModeEnabled <- GI.Gtk.getToggleButtonActive cropToggleButton
- if cropModeEnabled
+ updateVideoPreviewAspectRatio :: IO ()
+ updateVideoPreviewAspectRatio = do
+ cropModeEnabled <- GI.Gtk.getToggleButtonActive cropToggleButton
+ textOverlayModeEnabled <- GI.Gtk.getToggleButtonActive textOverlaysToggleButton
+ if cropModeEnabled || textOverlayModeEnabled
then
- void $ Data.GI.Base.Properties.setObjectPropertyBool
- playbinElement
- "force-aspect-ratio"
- False
+ void $ Data.GI.Base.Properties.setObjectPropertyBool
+ playbinElement
+ "force-aspect-ratio"
+ False
else
void $ Data.GI.Base.Properties.setObjectPropertyBool
playbinElement
@@ -328,8 +332,8 @@ firstAndLastFramePreview
inFilePath
startTime
durationTime
- _
- _
+ inVideoWidth
+ inVideoHeight
= do
GI.Gtk.widgetShow imagesPreviewBox
handleChanges
@@ -337,19 +341,18 @@ firstAndLastFramePreview
where
handleChanges :: IO ()
handleChanges = do
- let inFilePathChanged =
- fromMaybe "" maybeInFilePath /= inFilePath
- let startTimeChanged =
- fromMaybe (-1.0) maybeStartTime /= startTime
- let durationTimeChanged =
- fromMaybe (-1.0) maybeDurationTime /= durationTime
+ let inFilePathChanged = fromMaybe "" maybeInFilePath /= inFilePath
+ let startTimeChanged = fromMaybe (-1.0) maybeStartTime /= startTime
+ let durationTimeChanged = fromMaybe (-1.0) maybeDurationTime /= durationTime
let firstAndLastFrameDirty = inFilePathChanged || startTimeChanged
- let lastFrameDirty = not firstAndLastFrameDirty && durationTimeChanged
+ let lastFrameDirty = not firstAndLastFrameDirty && durationTimeChanged
+ let (previewWidth, _) = getPreviewWidthAndHeight inVideoWidth inVideoHeight
when firstAndLastFrameDirty $
makeFirstAndLastFramePreview
inFilePath
startTime
durationTime
+ (previewWidth / 2.0)
firstFrameImage
lastFrameImage
temporaryDirectory
@@ -359,6 +362,7 @@ firstAndLastFramePreview
inFilePath
startTime
durationTime
+ (previewWidth / 2.0)
lastFrameImage
temporaryDirectory
window
@@ -367,6 +371,69 @@ firstAndLastFramePreview
GI.Gtk.widgetQueueDraw firstFramePreviewImageDrawingArea
GI.Gtk.widgetQueueDraw lastFramePreviewImageDrawingArea
+runPreviewOverlay :: GR.GuiComponents -> IO ()
+runPreviewOverlay
+ guiComponents@GR.GuiComponents
+ { GR.maybeVideoPreviewWidget = (Just _)
+ , GR.maybePlaybinElement = (Just _)
+ , GR.videoPreviewDrawingArea
+ }
+ = do
+ _ <- GI.GLib.timeoutAdd
+ GI.GLib.PRIORITY_DEFAULT
+ 1 $ do
+ GI.Gtk.widgetQueueDraw videoPreviewDrawingArea
+ return True
+ onPreviewOverlayDraw
+ guiComponents
+ videoPreviewDrawingArea
+ ForFramePreviewNone
+ True
+ return ()
+runPreviewOverlay
+ guiComponents@GR.GuiComponents
+ { GR.firstFramePreviewImageDrawingArea
+ , GR.lastFramePreviewImageDrawingArea
+ }
+ = do
+ _ <- GI.GLib.timeoutAdd
+ GI.GLib.PRIORITY_DEFAULT
+ 1 $ do
+ GI.Gtk.widgetQueueDraw firstFramePreviewImageDrawingArea
+ GI.Gtk.widgetQueueDraw lastFramePreviewImageDrawingArea
+ return True
+ onPreviewOverlayDraw
+ guiComponents
+ firstFramePreviewImageDrawingArea
+ ForFramePreviewFirst
+ False
+ onPreviewOverlayDraw
+ guiComponents
+ lastFramePreviewImageDrawingArea
+ ForFramePreviewLast
+ False
+ return ()
+
+onPreviewOverlayDraw
+ :: GR.GuiComponents
+ -> GI.Gtk.DrawingArea
+ -> ForFramePreview
+ -> Bool
+ -> IO ()
+onPreviewOverlayDraw
+ guiComponents
+ drawingArea
+ forFramePreview
+ bool
+ =
+ void $
+ GI.Gtk.onWidgetDraw
+ drawingArea $
+ \ context -> do
+ _ <- drawCropGrid guiComponents drawingArea context
+ _ <- drawTextOverlays guiComponents drawingArea forFramePreview context
+ return bool
+
drawCropGrid :: GR.GuiComponents -> GI.Gtk.DrawingArea -> GI.Cairo.Context -> IO Bool
drawCropGrid
GR.GuiComponents
@@ -383,42 +450,459 @@ drawCropGrid
GR.InVideoProperties
{ GR.inVideoWidth
, GR.inVideoHeight
- } <- readIORef inVideoPropertiesRef
- cropModeEnabled <- GI.Gtk.getToggleButtonActive cropToggleButton
- drawingAreaWidth <-
+ } <- readIORef inVideoPropertiesRef
+ cropModeEnabled <- GI.Gtk.getToggleButtonActive cropToggleButton
+ drawingAreaWidth <-
int32ToDouble <$> GI.Gtk.widgetGetAllocatedWidth drawingArea
drawingAreaHeight <-
int32ToDouble <$> GI.Gtk.widgetGetAllocatedHeight drawingArea
- left <- (/ 100.0) <$> GI.Gtk.spinButtonGetValue leftCropSpinButton
- right <- (/ 100.0) <$> GI.Gtk.spinButtonGetValue rightCropSpinButton
- top <- (/ 100.0) <$> GI.Gtk.spinButtonGetValue topCropSpinButton
- bottom <- (/ 100.0) <$> GI.Gtk.spinButtonGetValue bottomCropSpinButton
- when (cropModeEnabled && inVideoWidth >= 0.0 && inVideoHeight >= 0.0) $
+ left <- GI.Gtk.spinButtonGetValue leftCropSpinButton
+ right <- GI.Gtk.spinButtonGetValue rightCropSpinButton
+ top <- GI.Gtk.spinButtonGetValue topCropSpinButton
+ bottom <- GI.Gtk.spinButtonGetValue bottomCropSpinButton
+ when (cropModeEnabled && inVideoWidth > 0.0 && inVideoHeight > 0.0) $
GiCairoCairoBridge.renderWithContext context $ do
- GRC.setSourceRGBA 0.0 0.0 0.0 0.8
- GRC.setLineWidth 1.0
- -- Left
- GRC.rectangle 0.0 0.0 (left * drawingAreaWidth) drawingAreaHeight
- GRC.fill
- -- Right
- GRC.rectangle
- (drawingAreaWidth - right * drawingAreaWidth)
- 0.0
- (right * drawingAreaWidth)
- drawingAreaHeight
- GRC.fill
- -- Top
- GRC.rectangle 0.0 0.0 drawingAreaWidth (top * drawingAreaHeight)
- GRC.fill
- -- Bottom
- GRC.rectangle
- 0.0
- (drawingAreaHeight - bottom * drawingAreaHeight)
- drawingAreaWidth
- (bottom * drawingAreaHeight)
- GRC.fill
+ orangePatternPng <- liftIO $ getDataFileName "data/orange-pattern.png"
+ orangeSurface <- liftIO $ GRC.imageSurfaceCreateFromPNG orangePatternPng
+ GRC.withPatternForSurface
+ orangeSurface $ \ orangePattern -> do
+ GRC.patternSetExtend orangePattern GRC.ExtendRepeat
+
+ -- Left
+ drawRectPattern
+ orangePattern
+ 0.0
+ 0.0
+ (left * drawingAreaWidth)
+ drawingAreaHeight
+ -- Right
+ drawRectPattern
+ orangePattern
+ (drawingAreaWidth - right * drawingAreaWidth)
+ 0.0
+ (right * drawingAreaWidth)
+ drawingAreaHeight
+ -- Top
+ drawRectPattern
+ orangePattern
+ 0.0
+ 0.0
+ drawingAreaWidth
+ (top * drawingAreaHeight)
+ -- Bottom
+ drawRectPattern
+ orangePattern
+ 0.0
+ (drawingAreaHeight - bottom * drawingAreaHeight)
+ drawingAreaWidth
+ (bottom * drawingAreaHeight)
return False
+drawTextOverlays
+ :: GR.GuiComponents
+ -> GI.Gtk.DrawingArea
+ -> ForFramePreview
+ -> GI.Cairo.Context
+ -> IO Bool
+drawTextOverlays
+ guiComponents@GR.GuiComponents
+ { GR.startTimeSpinButton
+ , GR.durationTimeSpinButton
+ , GR.textOverlaysToggleButton
+ , GR.inVideoPropertiesRef
+ }
+ drawingArea
+ forFramePreview
+ context
+ = do
+ GR.InVideoProperties
+ { GR.inVideoWidth
+ , GR.inVideoHeight
+ , GR.inVideoDuration
+ } <- readIORef inVideoPropertiesRef
+ textOverlayModeEnabled <- GI.Gtk.getToggleButtonActive textOverlaysToggleButton
+ when (textOverlayModeEnabled && inVideoWidth > 0.0 && inVideoHeight > 0.0) $ do
+ (videoDuration, videoPosition) <- getVideoDurationAndPosition (floatToDouble inVideoDuration)
+ drawingAreaWidth <- int32ToDouble <$> GI.Gtk.widgetGetAllocatedWidth drawingArea
+ drawingAreaHeight <- int32ToDouble <$> GI.Gtk.widgetGetAllocatedHeight drawingArea
+ GuiTextOverlays.updateTextOverlays False guiComponents
+ textOverlaysData <- GuiTextOverlays.getTextOverlaysData guiComponents
+ GiCairoCairoBridge.renderWithContext context $
+ mapM_
+ (renderTextOverlayData videoDuration videoPosition drawingAreaWidth drawingAreaHeight)
+ textOverlaysData
+ return False
+ where
+ renderTextOverlayData
+ :: Double
+ -> Double
+ -> Double
+ -> Double
+ -> GR.GuiTextOverlayData
+ -> GRC.Render ()
+ renderTextOverlayData
+ _videoDuration
+ videoPosition
+ drawingAreaWidth
+ drawingAreaHeight
+ GR.GuiTextOverlayData
+ { GR.textOverlayText
+ , GR.textOverlayLeft
+ , GR.textOverlayTop
+ , GR.textOverlayStartTime
+ , GR.textOverlayEndTime
+ , GR.textOverlayRotation
+ , GR.textOverlayOutlineSize
+ , GR.textOverlayOutlineColor
+ , GR.textOverlayFillColor
+ , GR.textOverlayMaybeFontDesc
+ }
+ = do
+ GRC.save
+
+ GRC.setLineWidth $ int32ToDouble textOverlayOutlineSize
+ pangoLayout <- GRPC.createLayout textOverlayText
+ liftIO $ GRPL.layoutSetAlignment pangoLayout GRPL.AlignCenter
+ liftIO $ GRPL.layoutSetFontDescription pangoLayout textOverlayMaybeFontDesc
+ (_, GRPL.PangoRectangle _x _y width height) <- liftIO $ GRPL.layoutGetExtents pangoLayout
+ let alphaChannel =
+ if textOverlayStartTime <= videoPosition
+ && textOverlayEndTime >= videoPosition
+ then 1.0
+ else 0.3
+ let x = (textOverlayLeft + 0.5) * drawingAreaWidth
+ let y = (textOverlayTop + 0.5) * drawingAreaHeight
+ let x' = x - (width / 2.0)
+ let y' = y - (height / 2.0)
+ let r = int32ToDouble textOverlayRotation * pi / 180.0
+
+ GRC.translate x y
+ GRC.rotate r
+ GRC.translate (-1 * x) (-1 * y)
+ GRC.translate x' y'
+
+ GRPC.layoutPath pangoLayout
+ (oR, oG, oB) <- liftIO $ getRgb textOverlayOutlineColor
+ GRC.setSourceRGBA oR oG oB alphaChannel
+ GRC.stroke
+ (fR, fG, fB) <- liftIO $ getRgb textOverlayFillColor
+ GRC.setSourceRGBA fR fG fB alphaChannel
+ GRPC.showLayout pangoLayout
+
+ GRC.restore
+ getRgb :: String -> IO (Double, Double, Double)
+ getRgb string = do
+ rgba <- GI.Gdk.newZeroRGBA
+ _ <- GI.Gdk.rGBAParse rgba (Data.Text.pack string)
+ r <- GI.Gdk.getRGBARed rgba
+ g <- GI.Gdk.getRGBAGreen rgba
+ b <- GI.Gdk.getRGBABlue rgba
+ return (r, g, b)
+ getVideoDurationAndPosition :: Double -> IO (Double, Double)
+ getVideoDurationAndPosition inVideoDuration = do
+ (playbinDuration, playbinPosition) <- getPlaybinDurationAndPosition guiComponents
+ case (playbinDuration, playbinPosition) of
+ (Just videoDuration, Just videoPosition) ->
+ return
+ ( nanosecondsToSeconds videoDuration
+ , nanosecondsToSeconds videoPosition
+ )
+ _ ->
+ case forFramePreview of
+ ForFramePreviewFirst -> do
+ videoPosition <- GI.Gtk.spinButtonGetValue startTimeSpinButton
+ return (inVideoDuration, videoPosition)
+ ForFramePreviewLast -> do
+ startTime <- GI.Gtk.spinButtonGetValue startTimeSpinButton
+ durationTime <- GI.Gtk.spinButtonGetValue durationTimeSpinButton
+ let videoPosition = startTime + durationTime
+ return (inVideoDuration, videoPosition)
+ ForFramePreviewNone -> return (0.0, -1.0)
+
+runTimeSlicesWidget :: GR.GuiComponents -> IO ()
+runTimeSlicesWidget
+ guiComponents@GR.GuiComponents
+ { GR.maybeVideoPreviewWidget
+ , GR.maybePlaybinElement
+ , GR.startTimeSpinButton
+ , GR.durationTimeSpinButton
+ , GR.timeSlicesDrawingArea
+ , GR.inVideoPropertiesRef
+ }
+ = do
+ when (isJust maybeVideoPreviewWidget && isJust maybePlaybinElement) $ do
+ GI.Gtk.widgetSetTooltipText
+ timeSlicesDrawingArea $
+ Just "Click to change the video position."
+ addOnMouseClickHandler
+ void $
+ GI.GLib.timeoutAdd
+ GI.GLib.PRIORITY_DEFAULT
+ 1 $ do
+ GI.Gtk.widgetQueueDraw timeSlicesDrawingArea
+ return True
+ void $
+ GI.Gtk.onWidgetDraw
+ timeSlicesDrawingArea $
+ \ context -> do
+ GR.InVideoProperties
+ { GR.inVideoDuration
+ } <- readIORef inVideoPropertiesRef
+ when (inVideoDuration > 0.0) $ do
+ startTime <- GI.Gtk.spinButtonGetValue startTimeSpinButton
+ durationTime <- GI.Gtk.spinButtonGetValue durationTimeSpinButton
+ textOverlaysData <- GuiTextOverlays.getTextOverlaysData guiComponents
+ drawingAreaWidth <- int32ToDouble <$> GI.Gtk.widgetGetAllocatedWidth timeSlicesDrawingArea
+ drawingAreaHeight <- int32ToDouble <$> GI.Gtk.widgetGetAllocatedHeight timeSlicesDrawingArea
+ let inVideoDuration' = floatToDouble inVideoDuration
+ let startTime' = startTime / inVideoDuration'
+ let durationTime' = durationTime / inVideoDuration'
+ GiCairoCairoBridge.renderWithContext context $ do
+ GRC.setSourceRGBA 0.1 0.1 0.1 1.0
+ GRC.rectangle 0.0 0.0 drawingAreaWidth drawingAreaHeight
+ GRC.fill
+
+ grayPatternPng <- liftIO $ getDataFileName "data/gray-pattern.png"
+ purplePatternPng <- liftIO $ getDataFileName "data/purple-pattern.png"
+ greenPatternPng <- liftIO $ getDataFileName "data/green-pattern.png"
+
+ graySurface <- liftIO $ GRC.imageSurfaceCreateFromPNG grayPatternPng
+ purpleSurface <- liftIO $ GRC.imageSurfaceCreateFromPNG purplePatternPng
+ greenSurface <- liftIO $ GRC.imageSurfaceCreateFromPNG greenPatternPng
+
+ GRC.withPatternForSurface
+ graySurface $ \ grayPattern ->
+ GRC.withPatternForSurface
+ purpleSurface $ \ purplePattern ->
+ GRC.withPatternForSurface
+ greenSurface $ \ greenPattern -> do
+ GRC.patternSetExtend grayPattern GRC.ExtendRepeat
+ GRC.patternSetExtend purplePattern GRC.ExtendRepeat
+ GRC.patternSetExtend greenPattern GRC.ExtendRepeat
+
+ -- Background.
+ drawRectPattern
+ grayPattern
+ 0.0
+ 0.0
+ drawingAreaWidth
+ drawingAreaHeight
+
+ -- GIF time slice.
+ drawRectPattern
+ purplePattern
+ 0.0
+ 0.0
+ drawingAreaWidth
+ (drawingAreaHeight / 2.0)
+
+ -- Video duration.
+ drawRectPattern
+ purplePattern
+ (startTime' * drawingAreaWidth)
+ (drawingAreaHeight / 2.0)
+ (durationTime' * drawingAreaWidth)
+ (drawingAreaHeight / 2.0)
+
+ mapM_
+ (\ GR.GuiTextOverlayData
+ { GR.textOverlayText
+ , GR.textOverlayStartTime
+ , GR.textOverlayDurationTime
+ }
+ ->
+ unless (Data.List.null textOverlayText) $ do
+ -- GIF time slice.
+ drawRectPattern
+ greenPattern
+ (((textOverlayStartTime - startTime) / durationTime) * drawingAreaWidth)
+ 0.0
+ ((textOverlayDurationTime / durationTime) * drawingAreaWidth)
+ (drawingAreaHeight / 2.0)
+
+ -- Video duration.
+ drawRectPattern
+ greenPattern
+ ((textOverlayStartTime / inVideoDuration') * drawingAreaWidth)
+ (drawingAreaHeight / 2.0)
+ ((textOverlayDurationTime / inVideoDuration') * drawingAreaWidth)
+ (drawingAreaHeight / 2.0)
+ )
+ textOverlaysData
+
+ (playbinDuration, playbinPosition) <-
+ liftIO $ getPlaybinDurationAndPosition guiComponents
+ case (playbinDuration, playbinPosition) of
+ (Just playbinDuration', Just playbinPosition') -> do
+ let videoDuration' = nanosecondsToSeconds playbinDuration'
+ let videoPosition' = nanosecondsToSeconds playbinPosition'
+ when (videoDuration' > 0.0) $ do
+ when (durationTime > 0.0) $ do
+ -- GIF time slice.
+ let videoPositionLineX =
+ ((videoPosition' - startTime) / durationTime) * drawingAreaWidth - 1.0
+ GRC.rectangle
+ videoPositionLineX
+ 0.0
+ 2.0
+ (drawingAreaHeight / 2.0)
+ GRC.setSourceRGBA 1.0 1.0 1.0 1.0
+ GRC.fill
+
+ GRC.selectFontFace
+ ("monospace" :: String)
+ GRC.FontSlantNormal
+ GRC.FontWeightNormal
+ GRC.setFontSize 15.0
+ let videoPosition'' = printf "%.2f" videoPosition' :: String
+ textWidth <- GRC.textExtentsWidth <$> GRC.textExtents videoPosition''
+ textHeight <- GRC.textExtentsHeight <$> GRC.textExtents videoPosition''
+ let textX = videoPositionLineX - (textWidth / 2.0)
+ let textY = drawingAreaHeight / 4.0 - (textHeight / 2.0)
+ GRC.rectangle textX textY (textWidth + 1.0) (textHeight + 1.0)
+ GRC.setSourceRGBA (48.0 / 255.0) (52/ 255.0) (58/ 255.0) 1.0
+ GRC.fill
+ GRC.setSourceRGBA 1.0 1.0 1.0 1.0
+ GRC.moveTo textX (textY + textHeight)
+ GRC.showText videoPosition''
+
+ -- Video duration.
+ GRC.rectangle
+ ((videoPosition' / videoDuration') * drawingAreaWidth - 1.0)
+ (drawingAreaHeight / 2.0)
+ 2.0
+ (drawingAreaHeight / 2.0)
+ GRC.setSourceRGBA 1.0 1.0 1.0 1.0
+ GRC.fill
+ _ -> return ()
+
+ -- Dividing line.
+ GRC.setSourceRGBA (48.0 / 255.0) (52/ 255.0) (58/ 255.0) 1.0
+ GRC.rectangle 0.0 (drawingAreaHeight / 2.0 - 1.0) drawingAreaWidth 2.0
+ GRC.fill
+ return True
+ where
+ addOnMouseClickHandler :: IO ()
+ addOnMouseClickHandler = do
+ GI.Gtk.widgetAddEvents timeSlicesDrawingArea [GI.Gdk.EventMaskAllEventsMask]
+ void $
+ GI.Gtk.onWidgetButtonReleaseEvent
+ timeSlicesDrawingArea $ \ eventButton -> do
+ eventButtonNumber <- GI.Gdk.getEventButtonButton eventButton
+ when (eventButtonNumber == 1) $ do
+ x <- GI.Gdk.getEventButtonX eventButton
+ y <- GI.Gdk.getEventButtonY eventButton
+ (playbinDuration, _) <- liftIO $ getPlaybinDurationAndPosition guiComponents
+ case (maybePlaybinElement, playbinDuration) of
+ (Just playbinElement, Just playbinDuration') -> do
+ let videoDuration' = nanosecondsToSeconds playbinDuration'
+ drawingAreaWidth <- int32ToDouble <$> GI.Gtk.widgetGetAllocatedWidth timeSlicesDrawingArea
+ drawingAreaHeight <- int32ToDouble <$> GI.Gtk.widgetGetAllocatedHeight timeSlicesDrawingArea
+ when (videoDuration' > 0.0 && drawingAreaWidth > 0.0) $ do
+ startTime <- GI.Gtk.spinButtonGetValue startTimeSpinButton
+ durationTime <- GI.Gtk.spinButtonGetValue durationTimeSpinButton
+ _ <- GI.Gst.elementSetState playbinElement GI.Gst.StatePaused
+ let seekPlaybinTo t =
+ void $
+ GI.Gst.elementSeekSimple
+ playbinElement
+ GI.Gst.FormatTime
+ [GI.Gst.SeekFlagsFlush]
+ t
+ if y <= (drawingAreaHeight / 2.0)
+ then do
+ let a = x / drawingAreaWidth
+ let b = a * durationTime
+ let c = startTime + b
+ let seekToInNano = secondsToNanoseconds $ doubleToFloat c
+ seekPlaybinTo seekToInNano
+ else do
+ let a = x / drawingAreaWidth
+ let b = a * videoDuration'
+ when (b >= startTime && b <= startTime + durationTime) $ do
+ let seekToInNano = secondsToNanoseconds $ doubleToFloat b
+ seekPlaybinTo seekToInNano
+ playPlaybinElement guiComponents
+ _ -> return ()
+ return True
+
+drawRectPattern
+ :: GRC.Pattern
+ -> Double
+ -> Double
+ -> Double
+ -> Double
+ -> GRC.Render ()
+drawRectPattern
+ p
+ x
+ y
+ w
+ h
+ = do
+ GRC.setSource p
+ GRC.rectangle x y w h
+ GRC.fill
+
+setupVideoPreviewPauseToggleButton :: GR.GuiComponents -> IO ()
+setupVideoPreviewPauseToggleButton
+ guiComponents@GR.GuiComponents
+ { GR.maybeVideoPreviewWidget = (Just _)
+ , GR.maybePlaybinElement = (Just playbinElement)
+ , GR.videoPreviewPauseToggleButton
+ }
+ = do
+ GI.Gtk.widgetShow videoPreviewPauseToggleButton
+ void $
+ GI.Gtk.afterToggleButtonToggled
+ videoPreviewPauseToggleButton $ do
+ active <- GI.Gtk.getToggleButtonActive videoPreviewPauseToggleButton
+ (_, playing, _) <- GI.Gst.elementGetState playbinElement (-1)
+ if active && playing == GI.Gst.StatePlaying
+ then void $ GI.Gst.elementSetState playbinElement GI.Gst.StatePaused
+ else playPlaybinElement guiComponents
+ if active
+ then GI.Gtk.setButtonLabel videoPreviewPauseToggleButton "Paused"
+ else GI.Gtk.setButtonLabel videoPreviewPauseToggleButton "Pause"
+setupVideoPreviewPauseToggleButton
+ GR.GuiComponents
+ { GR.videoPreviewPauseToggleButton
+ }
+ = GI.Gtk.widgetHide videoPreviewPauseToggleButton
+
+playPlaybinElement :: GR.GuiComponents -> IO ()
+playPlaybinElement
+ GR.GuiComponents
+ { GR.maybeVideoPreviewWidget = (Just _)
+ , GR.maybePlaybinElement = (Just playbinElement)
+ , GR.videoPreviewPauseToggleButton
+ }
+ = do
+ active <- GI.Gtk.getToggleButtonActive videoPreviewPauseToggleButton
+ (_, playing, _) <- GI.Gst.elementGetState playbinElement (-1)
+ when (not active && playing == GI.Gst.StatePaused) $
+ void $ GI.Gst.elementSetState playbinElement GI.Gst.StatePlaying
+playPlaybinElement _ = return ()
+
+getPlaybinDurationAndPosition :: GR.GuiComponents -> IO (Maybe Int64, Maybe Int64)
+getPlaybinDurationAndPosition
+ GR.GuiComponents
+ { GR.maybeVideoPreviewWidget = Just _videoPreviewWidget
+ , GR.maybePlaybinElement = Just playbinElement
+ }
+ = do
+ (couldQueryDuration, playbinDuration) <-
+ GI.Gst.elementQueryDuration playbinElement GI.Gst.FormatTime
+ (couldQueryPosition, playbinPosition) <-
+ GI.Gst.elementQueryPosition playbinElement GI.Gst.FormatTime
+ if couldQueryDuration
+ && couldQueryPosition
+ && playbinDuration > 0
+ && playbinPosition >= 0
+ then return (Just playbinDuration, Just playbinPosition)
+ else return (Nothing, Nothing)
+getPlaybinDurationAndPosition _ = return (Nothing, Nothing)
+
resetWindow :: GR.GuiComponents -> IO ()
resetWindow
GR.GuiComponents
@@ -430,7 +914,7 @@ resetWindow
= do
GI.Gtk.widgetSetSizeRequest
window
- 900
+ (floatToInt32 $ desiredPreviewSize + 300.0)
(-1)
GI.Gtk.widgetSetSizeRequest
mainPreviewBox
@@ -448,6 +932,7 @@ makeFirstAndLastFramePreview
:: String
-> Float
-> Float
+ -> Float
-> GI.Gtk.Image
-> GI.Gtk.Image
-> System.FilePath.FilePath
@@ -457,6 +942,7 @@ makeFirstAndLastFramePreview
inFilePath
startTime
durationTime
+ previewWidth
firstFrameImage
lastFrameImage
temporaryDirectory
@@ -475,13 +961,14 @@ makeFirstAndLastFramePreview
inFilePath
outFilePath
startTime
+ previewWidth
firstFrameImage
- ""
window
makeLastFramePreview
inFilePath
startTime
durationTime
+ previewWidth
lastFrameImage
temporaryDirectory
window
@@ -490,6 +977,7 @@ makeLastFramePreview
:: String
-> Float
-> Float
+ -> Float
-> GI.Gtk.Image
-> System.FilePath.FilePath
-> GI.Gtk.Window
@@ -498,6 +986,7 @@ makeLastFramePreview
inFilePath
startTime
durationTime
+ previewWidth
lastFrameImage
temporaryDirectory
window
@@ -516,8 +1005,8 @@ makeLastFramePreview
inFilePath
outFilePath
startTime'
+ previewWidth
lastFrameImage
- ""
window
setOrResetFramePrevew
@@ -525,11 +1014,11 @@ setOrResetFramePrevew
-> String
-> String
-> Float
+ -> Float
-> GI.Gtk.Image
- -> String
-> GI.Gtk.Window
-> IO ()
-setOrResetFramePrevew False _ _ _ image _ window =
+setOrResetFramePrevew False _ _ _ _ image window =
GtkMainSyncAsync.gtkMainAsync $ do
resetImage image
resizeWindow window
@@ -538,11 +1027,11 @@ setOrResetFramePrevew
inFilePath
outFilePath
time
+ previewWidth
image
- overlay
window
= do
- result <- makeImagePreview inFilePath outFilePath time overlay
+ result <- makeImagePreview inFilePath outFilePath time previewWidth
case result of
Left _ ->
void $ updatePreviewFrame "" image False
@@ -554,21 +1043,38 @@ makeImagePreview
:: String
-> String
-> Float
- -> String
+ -> Float
-> IO (Either IOError String)
-makeImagePreview inputFile outputFile startTime bottomText =
+makeImagePreview inputFile outputFile startTime previewWidth =
Gifcurry.gif $
Gifcurry.defaultGifParams
- { Gifcurry.inputFile = inputFile
- , Gifcurry.outputFile = outputFile
- , Gifcurry.saveAsVideo = False
- , Gifcurry.startTime = startTime
- , Gifcurry.durationTime = 0.001
- , Gifcurry.widthSize = 300
- , Gifcurry.qualityPercent = 50.0
- , Gifcurry.bottomText = bottomText
+ { Gifcurry.inputFile = inputFile
+ , Gifcurry.outputFile = outputFile
+ , Gifcurry.saveAsVideo = False
+ , Gifcurry.startTime = startTime
+ , Gifcurry.durationTime = 0.001
+ , Gifcurry.widthSize = round previewWidth :: Int
+ , Gifcurry.quality = Gifcurry.QualityLow
}
+getPreviewWidthAndHeight :: Float -> Float -> (Float, Float)
+getPreviewWidthAndHeight inVideoWidth inVideoHeight = (previewWidth, previewHeight)
+ where
+ widthRatio :: Float
+ widthRatio = inVideoWidth / inVideoHeight
+ heightRatio :: Float
+ heightRatio = inVideoHeight / inVideoWidth
+ previewWidth :: Float
+ previewWidth =
+ if inVideoWidth >= inVideoHeight
+ then desiredPreviewSize
+ else (desiredPreviewSize / 2.0) * widthRatio
+ previewHeight :: Float
+ previewHeight =
+ if inVideoWidth >= inVideoHeight
+ then desiredPreviewSize * heightRatio
+ else desiredPreviewSize / 2.0
+
updatePreviewFrame :: String -> GI.Gtk.Image -> Bool -> IO ()
updatePreviewFrame filePath image True =
GtkMainSyncAsync.gtkMainSync (GI.Gtk.imageSetFromFile image (Just filePath))
@@ -586,5 +1092,9 @@ secondsToNanoseconds :: Float -> Int64
secondsToNanoseconds s =
fromIntegral (round (s * 1000000000.0) :: Integer) :: Int64
+nanosecondsToSeconds :: Int64 -> Double
+nanosecondsToSeconds s =
+ int64ToDouble s * (1.0 / 1000000000.0)
+
desiredPreviewSize :: Float
-desiredPreviewSize = 600.0
+desiredPreviewSize = 700.0
diff --git a/src/gui/GuiRecords.hs b/src/gui/GuiRecords.hs
index 1bc384c..027f15e 100644
--- a/src/gui/GuiRecords.hs
+++ b/src/gui/GuiRecords.hs
@@ -7,7 +7,9 @@
module GuiRecords where
import Data.IORef
+import Data.Int
import qualified GI.Gtk
+import qualified Graphics.Rendering.Pango.Font as GRPF
import GI.Gst
data GuiComponents =
@@ -16,7 +18,6 @@ data GuiComponents =
, startTimeSpinButton :: GI.Gtk.SpinButton
, durationTimeSpinButton :: GI.Gtk.SpinButton
, widthSizeSpinButton :: GI.Gtk.SpinButton
- , qualityPercentSpinButton :: GI.Gtk.SpinButton
, leftCropSpinButton :: GI.Gtk.SpinButton
, rightCropSpinButton :: GI.Gtk.SpinButton
, topCropSpinButton :: GI.Gtk.SpinButton
@@ -25,57 +26,58 @@ data GuiComponents =
, inFileChooserDialogCancelButton :: GI.Gtk.Button
, inFileChooserDialogOpenButton :: GI.Gtk.Button
, outFileChooserButton :: GI.Gtk.FileChooserButton
- , fontChooserButton :: GI.Gtk.FontButton
+ , textOverlaysAddButton :: GI.Gtk.Button
, saveButton :: GI.Gtk.Button
, openButton :: GI.Gtk.Button
- , yesGtkButton :: GI.Gtk.Button
- , noGtkButton :: GI.Gtk.Button
+ , confirmMessageDialogYesButton :: GI.Gtk.Button
+ , confirmMessageDialogNoButton :: GI.Gtk.Button
, aboutButton :: GI.Gtk.Button
, giphyUploadButton :: GI.Gtk.Button
, imgurUploadButton :: GI.Gtk.Button
, saveAsVideoRadioButton :: GI.Gtk.RadioButton
- , widthQualityPercentToggleButton :: GI.Gtk.ToggleButton
+ , widthQualityToggleButton :: GI.Gtk.ToggleButton
, cropToggleButton :: GI.Gtk.ToggleButton
- , topBottomTextToggleButton :: GI.Gtk.ToggleButton
+ , textOverlaysToggleButton :: GI.Gtk.ToggleButton
, saveOpenToggleButton :: GI.Gtk.ToggleButton
, uploadToggleButton :: GI.Gtk.ToggleButton
+ , videoPreviewPauseToggleButton :: GI.Gtk.ToggleButton
, inFileChooserDialogLabel :: GI.Gtk.Label
, inFileChooserButtonLabel :: GI.Gtk.Label
, startTimeAdjustment :: GI.Gtk.Adjustment
, durationTimeAdjustment :: GI.Gtk.Adjustment
, widthSizeAdjustment :: GI.Gtk.Adjustment
- , qualityPercentAdjustment :: GI.Gtk.Adjustment
, outFileNameEntry :: GI.Gtk.Entry
- , topTextEntry :: GI.Gtk.Entry
- , bottomTextEntry :: GI.Gtk.Entry
, statusEntry :: GI.Gtk.Entry
+ , sidebarControlsPreviewbox :: GI.Gtk.Box
, mainPreviewBox :: GI.Gtk.Box
, imagesPreviewBox :: GI.Gtk.Box
, videoPreviewBox :: GI.Gtk.Box
, videoPreviewOverlayChildBox :: GI.Gtk.Box
- , widthQualityPercentBox :: GI.Gtk.Box
+ , widthQualityBox :: GI.Gtk.Box
, cropSpinButtonsBox :: GI.Gtk.Box
- , topBottomTextFontChooserBox :: GI.Gtk.Box
+ , textOverlaysMainBox :: GI.Gtk.Box
+ , textOverlaysBox :: GI.Gtk.Box
, saveOpenBox :: GI.Gtk.Box
, uploadBox :: GI.Gtk.Box
+ , qualityComboBoxText :: GI.Gtk.ComboBoxText
, videoPreviewDrawingArea :: GI.Gtk.DrawingArea
+ , timeSlicesDrawingArea :: GI.Gtk.DrawingArea
, firstFramePreviewImageDrawingArea :: GI.Gtk.DrawingArea
, lastFramePreviewImageDrawingArea :: GI.Gtk.DrawingArea
, inFileChooserButtonImage :: GI.Gtk.Image
, firstFrameImage :: GI.Gtk.Image
, lastFrameImage :: GI.Gtk.Image
, inFileChooserDialog :: GI.Gtk.Dialog
- , longGifGtkMessageDialog :: GI.Gtk.MessageDialog
+ , confirmMessageDialog :: GI.Gtk.MessageDialog
, aboutDialog :: GI.Gtk.AboutDialog
- , startTimeProgressBar :: GI.Gtk.ProgressBar
- , endTimeProgressBar :: GI.Gtk.ProgressBar
, saveSpinner :: GI.Gtk.Spinner
, inFileChooserWidget :: GI.Gtk.FileChooserWidget
, maybeVideoPreviewWidget :: Maybe GI.Gtk.Widget
, maybePlaybinElement :: Maybe GI.Gst.Element
, temporaryDirectory :: FilePath
- , guiPreviewStateRef :: IORef GuiPreviewState
+ , textOverlaysRef :: IORef [GuiTextOverlayComponents]
, inVideoPropertiesRef :: IORef InVideoProperties
+ , guiPreviewStateRef :: IORef GuiPreviewState
}
data GuiPreviewState =
@@ -94,20 +96,54 @@ data InVideoProperties =
, inVideoHeight :: Float
}
+data GuiTextOverlayComponents =
+ GuiTextOverlayComponents
+ { textOverlayId :: Int
+ , textOverlayBox :: GI.Gtk.Box
+ , textOverlayVisibilityBox :: GI.Gtk.Box
+ , textOverlayVisibilityToggleButton :: GI.Gtk.ToggleButton
+ , textOverlayLeftSpinButton :: GI.Gtk.SpinButton
+ , textOverlayTopSpinButton :: GI.Gtk.SpinButton
+ , textOverlayStartTimeSpinButton :: GI.Gtk.SpinButton
+ , textOverlayDurationTimeSpinButton :: GI.Gtk.SpinButton
+ , textOverlayRotationSpinButton :: GI.Gtk.SpinButton
+ , textOverlayOutlineSizeSpinButton :: GI.Gtk.SpinButton
+ , textOverlayOutlineColorButton :: GI.Gtk.ColorButton
+ , textOverlayFillColorButton :: GI.Gtk.ColorButton
+ , textOverlayTextEntry :: GI.Gtk.Entry
+ , textOverlayFontButton :: GI.Gtk.FontButton
+ , textOverlayRemoveButton :: GI.Gtk.Button
+ }
+
+data GuiTextOverlayData =
+ GuiTextOverlayData
+ { textOverlayText :: String
+ , textOverlayLeft :: Double
+ , textOverlayTop :: Double
+ , textOverlayStartTime :: Double
+ , textOverlayDurationTime :: Double
+ , textOverlayEndTime :: Double
+ , textOverlayRotation :: Int32
+ , textOverlayOutlineSize :: Int32
+ , textOverlayOutlineColor :: String
+ , textOverlayFillColor :: String
+ , textOverlayMaybeFontDesc :: Maybe GRPF.FontDescription
+ }
+
defaultGuiPreviewState :: GuiPreviewState
defaultGuiPreviewState =
GuiPreviewState
- { maybeInFilePath = Nothing
- , maybeStartTime = Nothing
+ { maybeInFilePath = Nothing
+ , maybeStartTime = Nothing
, maybeDurationTime = Nothing
- , loopRunning = False
+ , loopRunning = False
}
defaultInVideoProperties :: InVideoProperties
defaultInVideoProperties =
InVideoProperties
- { inVideoUri = ""
+ { inVideoUri = ""
, inVideoDuration = 0.0
- , inVideoWidth = 0.0
- , inVideoHeight = 0.0
+ , inVideoWidth = 0.0
+ , inVideoHeight = 0.0
}
diff --git a/src/gui/GuiStyle.hs b/src/gui/GuiStyle.hs
index 90f971f..6c771ef 100644
--- a/src/gui/GuiStyle.hs
+++ b/src/gui/GuiStyle.hs
@@ -13,6 +13,7 @@ import Control.Monad
import Data.Word
import Data.Text
import qualified Data.ByteString.Char8
+import qualified Data.ByteString
import qualified GI.Gdk
import qualified GI.Gtk
@@ -30,13 +31,46 @@ applyCss
maybeScreen <- GI.Gdk.screenGetDefault
provider <- GI.Gtk.cssProviderNew
styleFile <- getDataFileName "data/style.css"
- case maybeScreen of
- Just screen -> do
+ styleFile318 <- getDataFileName "data/style-3-18.css"
+ styleFile320 <- getDataFileName "data/style-3-20.css"
+ isGtkVersionGte310 <- isGtkVersionGte 3 10
+ isGtkVersionGte318 <- isGtkVersionGte 3 18
+ isGtkVersionGte320 <- isGtkVersionGte 3 20
+ case ( maybeScreen
+ , isGtkVersionGte310
+ , isGtkVersionGte318
+ , isGtkVersionGte320
+ )
+ of
+ (Just screen, True, False, False) -> do
GI.Gtk.cssProviderLoadFromPath provider (Data.Text.pack styleFile)
GI.Gtk.styleContextAddProviderForScreen
screen
provider
cssPriority
+ (Just screen, True, True, False) -> do
+ styleFileContents <- Data.ByteString.readFile styleFile
+ styleFileContents318 <- Data.ByteString.readFile styleFile318
+ let totalStyleFileContents =
+ Data.ByteString.concat
+ [styleFileContents, styleFileContents318]
+ GI.Gtk.cssProviderLoadFromData provider totalStyleFileContents
+ GI.Gtk.styleContextAddProviderForScreen
+ screen
+ provider
+ cssPriority
+ (Just screen, True, True, True) -> do
+ styleFileContents <- Data.ByteString.readFile styleFile
+ styleFileContents318 <- Data.ByteString.readFile styleFile318
+ styleFileContents320 <- Data.ByteString.readFile styleFile320
+ let totalStyleFileContents =
+ Data.ByteString.concat
+ [styleFileContents, styleFileContents318, styleFileContents320]
+ GI.Gtk.cssProviderLoadFromData provider totalStyleFileContents
+ GI.Gtk.styleContextAddProviderForScreen
+ screen
+ provider
+ cssPriority
_ -> return ()
styleWidget :: GI.Gtk.IsWidget a => String -> a -> IO ()
@@ -54,3 +88,9 @@ widgetAddStyleClass :: GI.Gtk.IsWidget a => a -> Text -> IO ()
widgetAddStyleClass widget styleClass = do
styleContext <- GI.Gtk.widgetGetStyleContext widget
GI.Gtk.styleContextAddClass styleContext styleClass
+
+isGtkVersionGte :: Word32 -> Word32 -> IO Bool
+isGtkVersionGte major minor = do
+ major' <- GI.Gtk.getMajorVersion
+ minor' <- GI.Gtk.getMinorVersion
+ return (major' >= major && minor' >= minor)
diff --git a/src/gui/GuiTextOverlays.hs b/src/gui/GuiTextOverlays.hs
new file mode 100644
index 0000000..04a20d3
--- /dev/null
+++ b/src/gui/GuiTextOverlays.hs
@@ -0,0 +1,620 @@
+{-
+ Gifcurry
+ (C) 2018 David Lettier
+ lettier.com
+-}
+
+{-# LANGUAGE
+ OverloadedStrings
+ , NamedFieldPuns
+ , BangPatterns
+#-}
+
+module GuiTextOverlays where
+
+import Control.Monad
+import Control.Exception
+import Data.Maybe
+import Data.IORef
+import qualified Data.Text
+import Data.List (sort)
+import qualified GI.Gdk
+import qualified GI.GdkPixbuf
+import qualified GI.Gtk
+import qualified GI.Pango
+import qualified Graphics.Rendering.Pango.Enums as GRPF
+import qualified Graphics.Rendering.Pango.Font as GRPF
+
+import Paths_Gifcurry
+import qualified Gifcurry
+import qualified GuiRecords as GR
+import GuiStyle
+import GuiMisc
+
+handleTextOverlaysAddButton :: GR.GuiComponents -> IO ()
+handleTextOverlaysAddButton
+ guiComponents@GR.GuiComponents
+ { GR.textOverlaysAddButton
+ }
+ =
+ void $
+ GI.Gtk.onButtonClicked
+ textOverlaysAddButton
+ (addTextOverlay guiComponents)
+
+getGifcurryTextOverlays :: GR.GuiComponents -> IO [Gifcurry.TextOverlay]
+getGifcurryTextOverlays
+ guiComponents@GR.GuiComponents
+ { GR.widthSizeSpinButton
+ }
+ = do
+ guiTextOverlaysData <- getTextOverlaysData guiComponents
+ (_, previewWidth, _) <- getPreviewDurationWidthAndHeight guiComponents
+ widthSelection <- GI.Gtk.spinButtonGetValue widthSizeSpinButton
+ mapM
+ ( getGifcurryTextOverlay
+ previewWidth
+ widthSelection
+ )
+ guiTextOverlaysData
+ where
+ getGifcurryTextOverlay
+ :: Double
+ -> Double
+ -> GR.GuiTextOverlayData
+ -> IO Gifcurry.TextOverlay
+ getGifcurryTextOverlay
+ previewWidth
+ gifWidth
+ GR.GuiTextOverlayData
+ { GR.textOverlayText
+ , GR.textOverlayLeft
+ , GR.textOverlayTop
+ , GR.textOverlayStartTime
+ , GR.textOverlayDurationTime
+ , GR.textOverlayRotation
+ , GR.textOverlayOutlineSize
+ , GR.textOverlayOutlineColor
+ , GR.textOverlayFillColor
+ , GR.textOverlayMaybeFontDesc
+ }
+ = do
+ newFontDesc <- GRPF.fontDescriptionNew
+ let fontDesc = fromMaybe newFontDesc textOverlayMaybeFontDesc
+ fontFamily <- fromMaybe "" <$> GRPF.fontDescriptionGetFamily fontDesc
+ fontStyle <- removeQuotes . show . fromMaybe GRPF.StyleNormal <$> GRPF.fontDescriptionGetStyle fontDesc
+ fontStretch <- removeQuotes . show . fromMaybe GRPF.StretchNormal <$> GRPF.fontDescriptionGetStretch fontDesc
+ fontWeight <- getFontWeight fontDesc
+ fontSize <- fromMaybe 30.0 <$> GRPF.fontDescriptionGetSize fontDesc
+ let fontSize' = doubleToInt $ fontSize * (gifWidth / previewWidth)
+ let xTranslate = doubleToFloat textOverlayLeft
+ let yTranslate = doubleToFloat textOverlayTop
+ let startTime = doubleToFloat textOverlayStartTime
+ let durationTime = doubleToFloat textOverlayDurationTime
+ let rotation = int32ToInt textOverlayRotation
+ let outlineSize = int32ToInt textOverlayOutlineSize
+ return
+ Gifcurry.TextOverlay
+ { Gifcurry.textOverlayText = textOverlayText
+ , Gifcurry.textOverlayFontFamily = fontFamily
+ , Gifcurry.textOverlayFontStyle = fontStyle
+ , Gifcurry.textOverlayFontStretch = fontStretch
+ , Gifcurry.textOverlayFontWeight = fontWeight
+ , Gifcurry.textOverlayFontSize = fontSize'
+ , Gifcurry.textOverlayOrigin = Gifcurry.TextOverlayOriginCenter
+ , Gifcurry.textOverlayXTranslation = xTranslate
+ , Gifcurry.textOverlayYTranslation = yTranslate
+ , Gifcurry.textOverlayRotation = rotation
+ , Gifcurry.textOverlayStartTime = startTime
+ , Gifcurry.textOverlayDurationTime = durationTime
+ , Gifcurry.textOverlayOutlineSize = outlineSize
+ , Gifcurry.textOverlayOutlineColor = textOverlayOutlineColor
+ , Gifcurry.textOverlayFillColor = textOverlayFillColor
+ }
+ -- `show` adds extra quotes to GRPF.Style* and GRPF.Stretch
+ removeQuotes :: String -> String
+ removeQuotes = foldl (\ xs x -> if x /= '\"' then xs ++ [x] else xs) ""
+ getFontWeight :: GRPF.FontDescription -> IO Int
+ getFontWeight fontDesc =
+ catch
+ getFontWeight'
+ (\ msg -> do
+ -- Some font weights, like 860, are not supported by the library.
+ putStrLn $ "[ERROR] " ++ show (msg :: SomeException)
+ return $ fromEnum GRPF.WeightNormal
+ )
+ where
+ getFontWeight' :: IO Int
+ getFontWeight' = do
+ !fontWeight <- fromEnum . fromMaybe GRPF.WeightNormal <$> GRPF.fontDescriptionGetWeight fontDesc
+ return fontWeight
+
+getTextOverlaysData :: GR.GuiComponents -> IO [GR.GuiTextOverlayData]
+getTextOverlaysData
+ GR.GuiComponents
+ { GR.textOverlaysRef
+ }
+ = do
+ textOverlays <- readIORef textOverlaysRef
+ mapM
+ (\
+ GR.GuiTextOverlayComponents
+ { GR.textOverlayTextEntry
+ , GR.textOverlayLeftSpinButton
+ , GR.textOverlayTopSpinButton
+ , GR.textOverlayStartTimeSpinButton
+ , GR.textOverlayDurationTimeSpinButton
+ , GR.textOverlayRotationSpinButton
+ , GR.textOverlayOutlineSizeSpinButton
+ , GR.textOverlayOutlineColorButton
+ , GR.textOverlayFillColorButton
+ , GR.textOverlayFontButton
+ }
+ -> do
+ text <- Data.Text.unpack <$> GI.Gtk.entryGetText textOverlayTextEntry
+ left <- GI.Gtk.spinButtonGetValue textOverlayLeftSpinButton
+ top <- GI.Gtk.spinButtonGetValue textOverlayTopSpinButton
+ start <- GI.Gtk.spinButtonGetValue textOverlayStartTimeSpinButton
+ duration <- GI.Gtk.spinButtonGetValue textOverlayDurationTimeSpinButton
+ rotation <- GI.Gtk.spinButtonGetValueAsInt textOverlayRotationSpinButton
+ outlineSize <- GI.Gtk.spinButtonGetValueAsInt textOverlayOutlineSizeSpinButton
+ outlineColor <- getColorButtonString textOverlayOutlineColorButton "rgba(0,0,0,1)"
+ fillColor <- getColorButtonString textOverlayFillColorButton "rgba(255,255,255,1)"
+ maybeFontDesc <- GI.Gtk.fontChooserGetFontDesc textOverlayFontButton
+ maybeFontDesc' <-
+ case maybeFontDesc of
+ Nothing -> return Nothing
+ Just fd -> do
+ fds <- Data.Text.unpack <$> GI.Pango.fontDescriptionToString fd
+ fd' <- GRPF.fontDescriptionFromString fds
+ return $ Just fd'
+ return
+ GR.GuiTextOverlayData
+ { GR.textOverlayText = text
+ , GR.textOverlayLeft = left
+ , GR.textOverlayTop = top
+ , GR.textOverlayStartTime = start
+ , GR.textOverlayDurationTime = duration
+ , GR.textOverlayRotation = rotation
+ , GR.textOverlayEndTime = start + duration
+ , GR.textOverlayOutlineSize = outlineSize
+ , GR.textOverlayOutlineColor = outlineColor
+ , GR.textOverlayFillColor = fillColor
+ , GR.textOverlayMaybeFontDesc = maybeFontDesc'
+ }
+ )
+ textOverlays
+
+updateTextOverlays :: Bool -> GR.GuiComponents -> IO ()
+updateTextOverlays
+ reset
+ guiComponents@GR.GuiComponents
+ { GR.textOverlaysRef
+ }
+ = do
+ textOverlays <- readIORef textOverlaysRef
+ (duration, _width, _height) <- getPreviewDurationWidthAndHeight guiComponents
+ mapM_ updateRotationOutlineSizeButtons textOverlays
+ mapM_ updatePositionSpinButtons textOverlays
+ mapM_ (updateTimeSpinButtons duration) textOverlays
+ when reset $
+ mapM_ clearEntry textOverlays
+ where
+ clearEntry :: GR.GuiTextOverlayComponents -> IO ()
+ clearEntry
+ GR.GuiTextOverlayComponents
+ { GR.textOverlayTextEntry
+ }
+ = GI.Gtk.entrySetText textOverlayTextEntry ""
+ updateRotationOutlineSizeButtons :: GR.GuiTextOverlayComponents -> IO ()
+ updateRotationOutlineSizeButtons
+ GR.GuiTextOverlayComponents
+ { GR.textOverlayRotationSpinButton
+ , GR.textOverlayOutlineSizeSpinButton
+ }
+ = do
+ rotation <- GI.Gtk.spinButtonGetValue textOverlayRotationSpinButton
+ GI.Gtk.entrySetProgressFraction
+ textOverlayRotationSpinButton $
+ rotation / 360.0
+ outlineSize <- GI.Gtk.spinButtonGetValue textOverlayOutlineSizeSpinButton
+ GI.Gtk.entrySetProgressFraction
+ textOverlayOutlineSizeSpinButton $
+ outlineSize / 10.0
+ when reset $ do
+ GI.Gtk.spinButtonSetValue textOverlayRotationSpinButton 0.0
+ GI.Gtk.spinButtonSetValue textOverlayOutlineSizeSpinButton 10.0
+ GI.Gtk.entrySetProgressFraction
+ textOverlayRotationSpinButton
+ 0.0
+ GI.Gtk.entrySetProgressFraction
+ textOverlayOutlineSizeSpinButton
+ 10.0
+ updatePositionSpinButtons :: GR.GuiTextOverlayComponents -> IO ()
+ updatePositionSpinButtons
+ GR.GuiTextOverlayComponents
+ { GR.textOverlayLeftSpinButton
+ , GR.textOverlayTopSpinButton
+ }
+ = do
+ GI.Gtk.spinButtonSetRange
+ textOverlayLeftSpinButton
+ (-0.5)
+ 0.5
+ GI.Gtk.spinButtonSetRange
+ textOverlayTopSpinButton
+ (-0.5)
+ 0.5
+ when reset $ do
+ GI.Gtk.spinButtonSetValue textOverlayLeftSpinButton 0.0
+ GI.Gtk.spinButtonSetValue textOverlayTopSpinButton 0.0
+ left <- GI.Gtk.spinButtonGetValue textOverlayLeftSpinButton
+ GI.Gtk.entrySetProgressFraction
+ textOverlayLeftSpinButton $
+ left + 0.5
+ top <- GI.Gtk.spinButtonGetValue textOverlayTopSpinButton
+ GI.Gtk.entrySetProgressFraction
+ textOverlayTopSpinButton $
+ top + 0.5
+ updateTimeSpinButtons :: Double -> GR.GuiTextOverlayComponents -> IO ()
+ updateTimeSpinButtons
+ duration
+ GR.GuiTextOverlayComponents
+ { GR.textOverlayStartTimeSpinButton
+ , GR.textOverlayDurationTimeSpinButton
+ }
+ = do
+ GI.Gtk.spinButtonSetRange
+ textOverlayStartTimeSpinButton
+ 0.0
+ duration
+ if reset
+ then do
+ GI.Gtk.spinButtonSetRange
+ textOverlayDurationTimeSpinButton
+ 0.0
+ duration
+ GI.Gtk.spinButtonSetValue
+ textOverlayStartTimeSpinButton
+ 0.0
+ GI.Gtk.spinButtonSetValue
+ textOverlayDurationTimeSpinButton $
+ truncatePastDigit duration 2
+ GI.Gtk.entrySetProgressFraction
+ textOverlayStartTimeSpinButton
+ 0.0
+ GI.Gtk.entrySetProgressFraction
+ textOverlayDurationTimeSpinButton
+ 1.0
+ else do
+ startTime <- GI.Gtk.spinButtonGetValue textOverlayStartTimeSpinButton
+ durationTime <- GI.Gtk.spinButtonGetValue textOverlayDurationTimeSpinButton
+ let maxDurationTime = clamp 0.0 duration (duration - startTime)
+ GI.Gtk.spinButtonSetRange
+ textOverlayDurationTimeSpinButton
+ 0.0
+ maxDurationTime
+ GI.Gtk.entrySetProgressFraction
+ textOverlayStartTimeSpinButton $
+ fromMaybe 0.0 $ safeDivide startTime duration
+ GI.Gtk.entrySetProgressFraction
+ textOverlayDurationTimeSpinButton $
+ fromMaybe 0.0 $ safeDivide durationTime maxDurationTime
+
+addTextOverlay :: GR.GuiComponents -> IO ()
+addTextOverlay
+ guiComponents@GR.GuiComponents
+ { GR.textOverlaysBox
+ , GR.confirmMessageDialog
+ , GR.textOverlaysRef
+ }
+ = do
+ (duration, width, height) <- getPreviewDurationWidthAndHeight guiComponents
+ when (duration > 0.0 && width > 0.0 && height > 0.0) $ do
+ isGtkVersionGte318 <- isGtkVersionGte 3 18
+ penIconFilePathName <- getDataFileName "data/pen-icon.svg"
+ rightIconFilePathName <- getDataFileName "data/right-icon.svg"
+ downIconFilePathName <- getDataFileName "data/down-icon.svg"
+ startIconFilePathName <- getDataFileName "data/start-icon.svg"
+ endIconFilePathName <- getDataFileName "data/end-icon.svg"
+ spiralIconFilePathName <- getDataFileName "data/spiral-icon.svg"
+ widthIconFilePathName <- getDataFileName "data/width-icon.svg"
+ minusIconFilePathName <- getDataFileName "data/minus-icon.svg"
+ rightIconPixbuf <- GI.GdkPixbuf.pixbufNewFromFile (Data.Text.pack rightIconFilePathName)
+ downIconPixbuf <- GI.GdkPixbuf.pixbufNewFromFile (Data.Text.pack downIconFilePathName)
+ startIconPixbuf <- GI.GdkPixbuf.pixbufNewFromFile (Data.Text.pack startIconFilePathName)
+ endIconPixbuf <- GI.GdkPixbuf.pixbufNewFromFile (Data.Text.pack endIconFilePathName)
+ spiralIconPixbuf <- GI.GdkPixbuf.pixbufNewFromFile (Data.Text.pack spiralIconFilePathName)
+ widthIconPixbuf <- GI.GdkPixbuf.pixbufNewFromFile (Data.Text.pack widthIconFilePathName)
+ penIconImage <- GI.Gtk.imageNewFromFile penIconFilePathName
+ minusIconImage <- GI.Gtk.imageNewFromFile minusIconFilePathName
+ box <- GI.Gtk.boxNew GI.Gtk.OrientationVertical 0
+ visibilityBox <- GI.Gtk.boxNew GI.Gtk.OrientationVertical 0
+ positionSpinButtonsBox <- GI.Gtk.boxNew GI.Gtk.OrientationHorizontal 0
+ timeSpinButtonsBox <- GI.Gtk.boxNew GI.Gtk.OrientationHorizontal 0
+ rotationOutlineSizeSpinButtonsBox <- GI.Gtk.boxNew GI.Gtk.OrientationHorizontal 0
+ colorButtonsBox <- GI.Gtk.boxNew GI.Gtk.OrientationHorizontal 0
+ leftAdjustment <- GI.Gtk.adjustmentNew 0.0 0.0 0.0 0.01 0.0 0.0
+ topAdjustment <- GI.Gtk.adjustmentNew 0.0 0.0 0.0 0.01 0.0 0.0
+ startTimeAdjustment <- GI.Gtk.adjustmentNew 0.0 0.0 0.0 1.0 0.0 0.0
+ durationTimeAdjustment <- GI.Gtk.adjustmentNew 0.0 0.0 0.0 1.0 0.0 0.0
+ rotationAdjustment <- GI.Gtk.adjustmentNew 0.0 0.0 360.0 1.0 0.0 0.0
+ outlineSizeAdjustment <- GI.Gtk.adjustmentNew 0.0 0.0 10.0 1.0 0.0 0.0
+ leftSpinButton <- GI.Gtk.spinButtonNew (Just leftAdjustment) 1.0 2
+ topSpinButton <- GI.Gtk.spinButtonNew (Just topAdjustment) 1.0 2
+ startTimeSpinButton <- GI.Gtk.spinButtonNew (Just startTimeAdjustment) 1.0 2
+ durationTimeSpinButton <- GI.Gtk.spinButtonNew (Just durationTimeAdjustment) 1.0 2
+ rotationSpinButton <- GI.Gtk.spinButtonNew (Just rotationAdjustment) 1.0 0
+ outlineSizeSpinButton <- GI.Gtk.spinButtonNew (Just outlineSizeAdjustment) 1.0 0
+ textEntry <- GI.Gtk.entryNew
+ visibilityToggleButton <- GI.Gtk.toggleButtonNew
+ fontButton <- GI.Gtk.fontButtonNew
+ blackRgba <- GI.Gdk.newZeroRGBA
+ whiteRgba <- GI.Gdk.newZeroRGBA
+ _ <- GI.Gdk.rGBAParse blackRgba "rgba(0,0,0,1)"
+ _ <- GI.Gdk.rGBAParse whiteRgba "rgba(255,255,255,1)"
+ outlineColorButton <- GI.Gtk.colorButtonNewWithRgba blackRgba
+ fillColorButton <- GI.Gtk.colorButtonNewWithRgba whiteRgba
+ removeButton <- GI.Gtk.buttonNewFromIconName (Just "gtk-remove") (enumToInt32 GI.Gtk.IconSizeButton)
+ fontDescription <- GI.Pango.fontDescriptionFromString "Sans Regular 30"
+ GI.Gtk.setToggleButtonDrawIndicator visibilityToggleButton False
+ GI.Gtk.setToggleButtonActive visibilityToggleButton True
+ GI.Gtk.buttonSetImage visibilityToggleButton (Just penIconImage)
+ GI.Gtk.buttonSetImage removeButton (Just minusIconImage)
+ GI.Gtk.setEntryPrimaryIconPixbuf leftSpinButton rightIconPixbuf
+ GI.Gtk.setEntryPrimaryIconPixbuf topSpinButton downIconPixbuf
+ GI.Gtk.setEntryPrimaryIconPixbuf startTimeSpinButton startIconPixbuf
+ GI.Gtk.setEntryPrimaryIconPixbuf durationTimeSpinButton endIconPixbuf
+ GI.Gtk.setEntryPrimaryIconPixbuf rotationSpinButton spiralIconPixbuf
+ GI.Gtk.setEntryPrimaryIconPixbuf outlineSizeSpinButton widthIconPixbuf
+ GI.Gtk.setWidgetDoubleBuffered visibilityToggleButton True
+ GI.Gtk.setButtonAlwaysShowImage visibilityToggleButton True
+ GI.Gtk.fontChooserSetFontDesc fontButton fontDescription
+ GI.Gtk.buttonSetLabel removeButton "Remove"
+ GI.Gtk.setButtonAlwaysShowImage removeButton True
+ GI.Gtk.boxSetHomogeneous positionSpinButtonsBox True
+ GI.Gtk.boxSetHomogeneous timeSpinButtonsBox True
+ GI.Gtk.boxSetHomogeneous rotationOutlineSizeSpinButtonsBox True
+ GI.Gtk.boxSetHomogeneous colorButtonsBox True
+ if isGtkVersionGte318
+ then do
+ GI.Gtk.widgetSetMarginEnd penIconImage 5
+ GI.Gtk.widgetSetMarginEnd minusIconImage 5
+ else do
+ -- To support GTK 3.10, Ubuntu 14.04.
+ GI.Gtk.widgetSetMarginRight penIconImage 5
+ GI.Gtk.widgetSetMarginRight minusIconImage 5
+ GI.Gtk.widgetSetTooltipText leftSpinButton $ Just "How much space from the left?"
+ GI.Gtk.widgetSetTooltipText topSpinButton $ Just "How much space from the top?"
+ GI.Gtk.widgetSetTooltipText startTimeSpinButton $ Just "What is the start time?"
+ GI.Gtk.widgetSetTooltipText durationTimeSpinButton $ Just "How long is the duration?"
+ GI.Gtk.widgetSetTooltipText rotationSpinButton $ Just "Rotate by how much?"
+ GI.Gtk.widgetSetTooltipText outlineSizeSpinButton $ Just "How thick is the outline size?"
+ GI.Gtk.widgetSetTooltipText outlineColorButton $ Just "What is the outline color?"
+ GI.Gtk.widgetSetTooltipText fillColorButton $ Just "What is the fill color?"
+ GI.Gtk.widgetSetTooltipText removeButton $ Just "Remove this text?"
+ GI.Gtk.entrySetPlaceholderText textEntry $ Just "What is the text?"
+ GI.Gtk.spinButtonSetNumeric leftSpinButton True
+ GI.Gtk.spinButtonSetNumeric topSpinButton True
+ GI.Gtk.spinButtonSetNumeric startTimeSpinButton True
+ GI.Gtk.spinButtonSetNumeric durationTimeSpinButton True
+ GI.Gtk.spinButtonSetNumeric rotationSpinButton True
+ GI.Gtk.spinButtonSetNumeric outlineSizeSpinButton True
+ GI.Gtk.spinButtonSetUpdatePolicy leftSpinButton GI.Gtk.SpinButtonUpdatePolicyIfValid
+ GI.Gtk.spinButtonSetUpdatePolicy topSpinButton GI.Gtk.SpinButtonUpdatePolicyIfValid
+ GI.Gtk.spinButtonSetUpdatePolicy startTimeSpinButton GI.Gtk.SpinButtonUpdatePolicyIfValid
+ GI.Gtk.spinButtonSetUpdatePolicy durationTimeSpinButton GI.Gtk.SpinButtonUpdatePolicyIfValid
+ GI.Gtk.spinButtonSetUpdatePolicy rotationSpinButton GI.Gtk.SpinButtonUpdatePolicyIfValid
+ GI.Gtk.spinButtonSetUpdatePolicy outlineSizeSpinButton GI.Gtk.SpinButtonUpdatePolicyIfValid
+ GI.Gtk.spinButtonSetRange leftSpinButton (-0.5) 0.5
+ GI.Gtk.spinButtonSetRange topSpinButton (-0.5) 0.5
+ GI.Gtk.spinButtonSetRange startTimeSpinButton 0.0 duration
+ GI.Gtk.spinButtonSetRange durationTimeSpinButton 0.0 duration
+ GI.Gtk.spinButtonSetRange rotationSpinButton 0.0 360.0
+ GI.Gtk.spinButtonSetRange outlineSizeSpinButton 0.0 10.0
+ GI.Gtk.spinButtonSetValue leftSpinButton 0.0
+ GI.Gtk.spinButtonSetValue topSpinButton 0.0
+ GI.Gtk.spinButtonSetValue startTimeSpinButton 0.0
+ GI.Gtk.spinButtonSetValue durationTimeSpinButton duration
+ GI.Gtk.spinButtonSetValue rotationSpinButton 0.0
+ GI.Gtk.spinButtonSetValue outlineSizeSpinButton 10.0
+ GI.Gtk.entrySetProgressFraction leftSpinButton 0.5
+ GI.Gtk.entrySetProgressFraction topSpinButton 0.5
+ GI.Gtk.entrySetProgressFraction startTimeSpinButton 0.0
+ GI.Gtk.entrySetProgressFraction durationTimeSpinButton 1.0
+ GI.Gtk.entrySetProgressFraction rotationSpinButton 0.0
+ GI.Gtk.entrySetProgressFraction outlineSizeSpinButton 1.0
+ GI.Gtk.widgetSetMarginTop visibilityToggleButton 0
+ GI.Gtk.widgetSetMarginBottom removeButton 0
+ GI.Gtk.widgetShow box
+ GI.Gtk.widgetShow visibilityBox
+ GI.Gtk.widgetShow visibilityToggleButton
+ GI.Gtk.widgetShow positionSpinButtonsBox
+ GI.Gtk.widgetShow timeSpinButtonsBox
+ GI.Gtk.widgetShow rotationOutlineSizeSpinButtonsBox
+ GI.Gtk.widgetShow colorButtonsBox
+ GI.Gtk.widgetShow leftSpinButton
+ GI.Gtk.widgetShow topSpinButton
+ GI.Gtk.widgetShow startTimeSpinButton
+ GI.Gtk.widgetShow durationTimeSpinButton
+ GI.Gtk.widgetShow rotationSpinButton
+ GI.Gtk.widgetShow outlineSizeSpinButton
+ GI.Gtk.widgetShow outlineColorButton
+ GI.Gtk.widgetShow fillColorButton
+ GI.Gtk.widgetShow textEntry
+ GI.Gtk.widgetShow fontButton
+ GI.Gtk.widgetShow removeButton
+ GI.Gtk.containerAdd positionSpinButtonsBox leftSpinButton
+ GI.Gtk.containerAdd positionSpinButtonsBox topSpinButton
+ GI.Gtk.containerAdd timeSpinButtonsBox startTimeSpinButton
+ GI.Gtk.containerAdd timeSpinButtonsBox durationTimeSpinButton
+ GI.Gtk.containerAdd rotationOutlineSizeSpinButtonsBox rotationSpinButton
+ GI.Gtk.containerAdd rotationOutlineSizeSpinButtonsBox outlineSizeSpinButton
+ GI.Gtk.containerAdd colorButtonsBox outlineColorButton
+ GI.Gtk.containerAdd colorButtonsBox fillColorButton
+ GI.Gtk.containerAdd visibilityBox positionSpinButtonsBox
+ GI.Gtk.containerAdd visibilityBox timeSpinButtonsBox
+ GI.Gtk.containerAdd visibilityBox rotationOutlineSizeSpinButtonsBox
+ GI.Gtk.containerAdd visibilityBox colorButtonsBox
+ GI.Gtk.containerAdd visibilityBox textEntry
+ GI.Gtk.containerAdd visibilityBox fontButton
+ GI.Gtk.containerAdd visibilityBox removeButton
+ GI.Gtk.containerAdd box visibilityToggleButton
+ GI.Gtk.containerAdd box visibilityBox
+ GI.Gtk.containerAdd textOverlaysBox box
+ textOverlays <- readIORef textOverlaysRef
+ let ids = sort $ map GR.textOverlayId textOverlays
+ let (_, minId) =
+ foldl
+ (\ (i, m) i' -> if i == i' then (i + 1, i + 1) else (i + 1, m))
+ (0, 0)
+ ids
+ let minIdText = Data.Text.pack $ show minId
+ GI.Gtk.buttonSetLabel visibilityToggleButton minIdText
+ GI.Gtk.widgetSetTooltipText visibilityToggleButton $
+ Just $
+ Data.Text.concat ["Close text ", minIdText, "?"]
+ let textOverlay =
+ GR.GuiTextOverlayComponents
+ { GR.textOverlayId = minId
+ , GR.textOverlayBox = box
+ , GR.textOverlayVisibilityBox = visibilityBox
+ , GR.textOverlayVisibilityToggleButton = visibilityToggleButton
+ , GR.textOverlayLeftSpinButton = leftSpinButton
+ , GR.textOverlayTopSpinButton = topSpinButton
+ , GR.textOverlayStartTimeSpinButton = startTimeSpinButton
+ , GR.textOverlayDurationTimeSpinButton = durationTimeSpinButton
+ , GR.textOverlayRotationSpinButton = rotationSpinButton
+ , GR.textOverlayOutlineSizeSpinButton = outlineSizeSpinButton
+ , GR.textOverlayOutlineColorButton = outlineColorButton
+ , GR.textOverlayFillColorButton = fillColorButton
+ , GR.textOverlayTextEntry = textEntry
+ , GR.textOverlayFontButton = fontButton
+ , GR.textOverlayRemoveButton = removeButton
+ }
+ atomicModifyIORef' textOverlaysRef $
+ \ textOverlays' ->
+ ( textOverlays' ++ [textOverlay]
+ , ()
+ )
+ _ <- GI.Gtk.onButtonClicked removeButton $ do
+ GI.Gtk.setMessageDialogText
+ confirmMessageDialog $
+ Data.Text.concat ["Remove text ", minIdText, "?"]
+ confirmMessageDialogResponse <- GI.Gtk.dialogRun confirmMessageDialog
+ when (confirmMessageDialogResponse == enumToInt32 GI.Gtk.ResponseTypeYes) $ do
+ textOverlays' <- filterTextOverlay minId <$> readIORef textOverlaysRef
+ atomicModifyIORef' textOverlaysRef $
+ const (textOverlays', ())
+ GI.Gtk.containerRemove textOverlaysBox box
+ _ <- GI.Gtk.onWidgetButtonReleaseEvent visibilityToggleButton $ \ _ -> do
+ active <- GI.Gtk.getToggleButtonActive visibilityToggleButton
+ if active
+ then hideAllOtherTextOverlays guiComponents (-1)
+ else hideAllOtherTextOverlays guiComponents minId
+ return True
+ _ <- GI.Gtk.afterWidgetKeyReleaseEvent textEntry $ \ _ -> do
+ text <- GI.Gtk.entryGetText textEntry
+ let limit = 27
+ let label = Data.Text.concat [minIdText, " ", text]
+ let label' =
+ if Data.Text.length label >= limit
+ then Data.Text.concat [Data.Text.take limit label, "..."]
+ else label
+ GI.Gtk.buttonSetLabel
+ visibilityToggleButton
+ label'
+ return False
+ updateTextOverlays False guiComponents
+ hideAllOtherTextOverlays guiComponents minId
+ return ()
+ where
+ filterTextOverlay :: Int -> [GR.GuiTextOverlayComponents] -> [GR.GuiTextOverlayComponents]
+ filterTextOverlay textOverlayId =
+ foldl
+ (\ x t -> if textOverlayId /= GR.textOverlayId t then x ++ [t] else x)
+ []
+
+removeTextOverlays :: GR.GuiComponents -> IO ()
+removeTextOverlays
+ GR.GuiComponents
+ { GR.textOverlaysBox
+ , GR.textOverlaysRef
+ }
+ = do
+ textOverlays <- readIORef textOverlaysRef
+ mapM_
+ (\ GR.GuiTextOverlayComponents { GR.textOverlayBox } ->
+ GI.Gtk.containerRemove textOverlaysBox textOverlayBox
+ )
+ textOverlays
+ atomicModifyIORef' textOverlaysRef $ const ([], ())
+
+hideAllOtherTextOverlays :: GR.GuiComponents -> Int -> IO ()
+hideAllOtherTextOverlays
+ GR.GuiComponents
+ { GR.textOverlaysRef
+ }
+ showTextOverlayId
+ = do
+ textOverlays <- readIORef textOverlaysRef
+ mapM_
+ (\ GR.GuiTextOverlayComponents
+ { GR.textOverlayId
+ , GR.textOverlayVisibilityBox
+ , GR.textOverlayVisibilityToggleButton
+ }
+ -> do
+ let textOverlayId' = Data.Text.pack $ show textOverlayId
+ if textOverlayId == showTextOverlayId
+ then do
+ GI.Gtk.widgetShow textOverlayVisibilityBox
+ GI.Gtk.setToggleButtonActive textOverlayVisibilityToggleButton True
+ GI.Gtk.widgetSetTooltipText textOverlayVisibilityToggleButton $
+ Just $
+ Data.Text.concat ["Close text ", textOverlayId', "?"]
+ else do
+ GI.Gtk.widgetHide textOverlayVisibilityBox
+ GI.Gtk.setToggleButtonActive textOverlayVisibilityToggleButton False
+ GI.Gtk.widgetSetTooltipText textOverlayVisibilityToggleButton $
+ Just $
+ Data.Text.concat ["Open text ", textOverlayId', "?"]
+ )
+ textOverlays
+ return ()
+
+getPreviewDurationWidthAndHeight :: GR.GuiComponents -> IO (Double, Double, Double)
+getPreviewDurationWidthAndHeight
+ GR.GuiComponents
+ { GR.maybeVideoPreviewWidget
+ , GR.videoPreviewDrawingArea
+ , GR.firstFramePreviewImageDrawingArea
+ , GR.inVideoPropertiesRef
+ }
+ = do
+ GR.InVideoProperties
+ { GR.inVideoDuration
+ , GR.inVideoWidth
+ , GR.inVideoHeight
+ } <- readIORef inVideoPropertiesRef
+ let usingVideoPreview = isJust maybeVideoPreviewWidget
+ let videoHasSize = if inVideoWidth > 0.0 && inVideoHeight > 0.0 then 1.0 else 0.0
+ let drawingArea = if usingVideoPreview
+ then videoPreviewDrawingArea
+ else firstFramePreviewImageDrawingArea
+ width <- (*) videoHasSize . int32ToDouble <$> GI.Gtk.widgetGetAllocatedWidth drawingArea
+ height <- (*) videoHasSize . int32ToDouble <$> GI.Gtk.widgetGetAllocatedHeight drawingArea
+ let videoDuration = floatToDouble inVideoDuration
+ return (videoDuration, width, height)
+
+getColorButtonString :: GI.Gtk.ColorButton -> String -> IO String
+getColorButtonString colorButton defaultString = do
+ defaultRgba <- GI.Gdk.newZeroRGBA
+ _ <- GI.Gdk.rGBAParse defaultRgba (Data.Text.pack defaultString)
+ maybeRgba <- GI.Gtk.getColorButtonRgba colorButton
+ text <-
+ case maybeRgba of
+ Just rgba -> GI.Gdk.rGBAToString rgba
+ Nothing -> GI.Gdk.rGBAToString defaultRgba
+ return $ Data.Text.unpack text
+
diff --git a/src/gui/Main.hs b/src/gui/Main.hs
index fd4130b..a3dd597 100644
--- a/src/gui/Main.hs
+++ b/src/gui/Main.hs
@@ -34,17 +34,20 @@ import Paths_Gifcurry
import qualified Gifcurry
( gif
, GifParams(..)
+ , Quality(QualityMedium)
, defaultGifParams
, gifParamsValid
, getVideoDurationInSeconds
, getOutputFileWithExtension
, getVideoWidthAndHeight
, findOrCreateTemporaryDirectory
+ , qualityFromString
)
import qualified GtkMainSyncAsync (gtkMainAsync)
import qualified GuiRecords as GR
import qualified GuiCapabilities
import qualified GuiStyle
+import qualified GuiTextOverlays
import qualified GuiPreview
import GuiMisc
@@ -62,7 +65,6 @@ main = do
startTimeSpinButton <- builderGetObject GI.Gtk.SpinButton builder "start-time-spin-button"
durationTimeSpinButton <- builderGetObject GI.Gtk.SpinButton builder "duration-time-spin-button"
widthSizeSpinButton <- builderGetObject GI.Gtk.SpinButton builder "width-size-spin-button"
- qualityPercentSpinButton <- builderGetObject GI.Gtk.SpinButton builder "quality-percent-spin-button"
leftCropSpinButton <- builderGetObject GI.Gtk.SpinButton builder "left-crop-spin-button"
rightCropSpinButton <- builderGetObject GI.Gtk.SpinButton builder "right-crop-spin-button"
topCropSpinButton <- builderGetObject GI.Gtk.SpinButton builder "top-crop-spin-button"
@@ -71,56 +73,56 @@ main = do
inFileChooserDialogCancelButton <- builderGetObject GI.Gtk.Button builder "in-file-chooser-dialog-cancel-button"
inFileChooserDialogOpenButton <- builderGetObject GI.Gtk.Button builder "in-file-chooser-dialog-open-button"
outFileChooserButton <- builderGetObject GI.Gtk.FileChooserButton builder "out-file-chooser-button"
- fontChooserButton <- builderGetObject GI.Gtk.FontButton builder "font-chooser-button"
+ textOverlaysAddButton <- builderGetObject GI.Gtk.Button builder "text-overlays-add-button"
saveButton <- builderGetObject GI.Gtk.Button builder "save-button"
openButton <- builderGetObject GI.Gtk.Button builder "open-button"
- yesGtkButton <- builderGetObject GI.Gtk.Button builder "yes-button"
- noGtkButton <- builderGetObject GI.Gtk.Button builder "no-button"
+ confirmMessageDialogYesButton <- builderGetObject GI.Gtk.Button builder "confirm-message-dialog-yes-button"
+ confirmMessageDialogNoButton <- builderGetObject GI.Gtk.Button builder "confirm-message-dialog-no-button"
aboutButton <- builderGetObject GI.Gtk.Button builder "about-button"
giphyUploadButton <- builderGetObject GI.Gtk.Button builder "giphy-upload-button"
imgurUploadButton <- builderGetObject GI.Gtk.Button builder "imgur-upload-button"
saveAsVideoRadioButton <- builderGetObject GI.Gtk.RadioButton builder "save-as-video-radio-button"
- widthQualityPercentToggleButton <- builderGetObject GI.Gtk.ToggleButton builder "width-quality-percent-toggle-button"
+ widthQualityToggleButton <- builderGetObject GI.Gtk.ToggleButton builder "width-quality-toggle-button"
cropToggleButton <- builderGetObject GI.Gtk.ToggleButton builder "crop-toggle-button"
- topBottomTextToggleButton <- builderGetObject GI.Gtk.ToggleButton builder "top-bottom-text-toggle-button"
+ textOverlaysToggleButton <- builderGetObject GI.Gtk.ToggleButton builder "text-overlays-toggle-button"
saveOpenToggleButton <- builderGetObject GI.Gtk.ToggleButton builder "save-open-toggle-button"
uploadToggleButton <- builderGetObject GI.Gtk.ToggleButton builder "upload-toggle-button"
+ videoPreviewPauseToggleButton <- builderGetObject GI.Gtk.ToggleButton builder "video-preview-pause-toggle-button"
inFileChooserDialogLabel <- builderGetObject GI.Gtk.Label builder "in-file-chooser-dialog-label"
inFileChooserButtonLabel <- builderGetObject GI.Gtk.Label builder "in-file-chooser-button-label"
startTimeAdjustment <- builderGetObject GI.Gtk.Adjustment builder "start-time-adjustment"
durationTimeAdjustment <- builderGetObject GI.Gtk.Adjustment builder "duration-time-adjustment"
widthSizeAdjustment <- builderGetObject GI.Gtk.Adjustment builder "width-size-adjustment"
- qualityPercentAdjustment <- builderGetObject GI.Gtk.Adjustment builder "quality-percent-adjustment"
outFileNameEntry <- builderGetObject GI.Gtk.Entry builder "out-file-name-entry"
- topTextEntry <- builderGetObject GI.Gtk.Entry builder "top-text-entry"
- bottomTextEntry <- builderGetObject GI.Gtk.Entry builder "bottom-text-entry"
statusEntry <- builderGetObject GI.Gtk.Entry builder "status-entry"
+ sidebarControlsPreviewbox <- builderGetObject GI.Gtk.Box builder "sidebar-controls-preview-box"
mainPreviewBox <- builderGetObject GI.Gtk.Box builder "main-preview-box"
imagesPreviewBox <- builderGetObject GI.Gtk.Box builder "images-preview-box"
videoPreviewBox <- builderGetObject GI.Gtk.Box builder "video-preview-box"
videoPreviewOverlayChildBox <- builderGetObject GI.Gtk.Box builder "video-preview-overlay-child-box"
- widthQualityPercentBox <- builderGetObject GI.Gtk.Box builder "width-quality-percent-box"
+ widthQualityBox <- builderGetObject GI.Gtk.Box builder "width-quality-box"
cropSpinButtonsBox <- builderGetObject GI.Gtk.Box builder "crop-spin-buttons-box"
- topBottomTextFontChooserBox <- builderGetObject GI.Gtk.Box builder "top-bottom-text-font-chooser-box"
+ textOverlaysMainBox <- builderGetObject GI.Gtk.Box builder "text-overlays-main-box"
+ textOverlaysBox <- builderGetObject GI.Gtk.Box builder "text-overlays-box"
saveOpenBox <- builderGetObject GI.Gtk.Box builder "save-open-box"
uploadBox <- builderGetObject GI.Gtk.Box builder "upload-box"
+ qualityComboBoxText <- builderGetObject GI.Gtk.ComboBoxText builder "quality-combo-box-text"
videoPreviewDrawingArea <- builderGetObject GI.Gtk.DrawingArea builder "video-preview-drawing-area"
+ timeSlicesDrawingArea <- builderGetObject GI.Gtk.DrawingArea builder "time-slices-drawing-area"
firstFramePreviewImageDrawingArea <- builderGetObject GI.Gtk.DrawingArea builder "first-frame-preview-image-drawing-area"
lastFramePreviewImageDrawingArea <- builderGetObject GI.Gtk.DrawingArea builder "last-frame-preview-image-drawing-area"
inFileChooserButtonImage <- builderGetObject GI.Gtk.Image builder "in-file-chooser-button-image"
firstFrameImage <- builderGetObject GI.Gtk.Image builder "first-frame-image"
lastFrameImage <- builderGetObject GI.Gtk.Image builder "last-frame-image"
inFileChooserDialog <- builderGetObject GI.Gtk.Dialog builder "in-file-chooser-dialog"
- longGifGtkMessageDialog <- builderGetObject GI.Gtk.MessageDialog builder "long-gif-message-dialog"
+ confirmMessageDialog <- builderGetObject GI.Gtk.MessageDialog builder "confirm-message-dialog"
aboutDialog <- builderGetObject GI.Gtk.AboutDialog builder "about-dialog"
- startTimeProgressBar <- builderGetObject GI.Gtk.ProgressBar builder "start-time-progress-bar"
- endTimeProgressBar <- builderGetObject GI.Gtk.ProgressBar builder "end-time-progress-bar"
saveSpinner <- builderGetObject GI.Gtk.Spinner builder "save-spinner"
inFileChooserWidget <- builderGetObject GI.Gtk.FileChooserWidget builder "in-file-chooser-widget"
-- Glade does not allow us to use the response ID nicknames so we set them here.
- GI.Gtk.dialogAddActionWidget longGifGtkMessageDialog yesGtkButton $ enumToInt32 GI.Gtk.ResponseTypeYes
- GI.Gtk.dialogAddActionWidget longGifGtkMessageDialog noGtkButton $ enumToInt32 GI.Gtk.ResponseTypeNo
+ GI.Gtk.dialogAddActionWidget confirmMessageDialog confirmMessageDialogYesButton $ enumToInt32 GI.Gtk.ResponseTypeYes
+ GI.Gtk.dialogAddActionWidget confirmMessageDialog confirmMessageDialogNoButton $ enumToInt32 GI.Gtk.ResponseTypeNo
GI.Gtk.dialogAddActionWidget inFileChooserDialog inFileChooserDialogCancelButton $ enumToInt32 GI.Gtk.ResponseTypeCancel
GI.Gtk.dialogAddActionWidget inFileChooserDialog inFileChooserDialogOpenButton $ enumToInt32 GI.Gtk.ResponseTypeOk
@@ -129,9 +131,9 @@ main = do
temporaryDirectory <- Gifcurry.findOrCreateTemporaryDirectory
- guiPreviewStateRef <- newIORef GR.defaultGuiPreviewState
-
inVideoPropertiesRef <- newIORef GR.defaultInVideoProperties
+ textOverlaysRef <- newIORef []
+ guiPreviewStateRef <- newIORef GR.defaultGuiPreviewState
let guiComponents =
GR.GuiComponents
@@ -139,7 +141,6 @@ main = do
, GR.startTimeSpinButton = startTimeSpinButton
, GR.durationTimeSpinButton = durationTimeSpinButton
, GR.widthSizeSpinButton = widthSizeSpinButton
- , GR.qualityPercentSpinButton = qualityPercentSpinButton
, GR.leftCropSpinButton = leftCropSpinButton
, GR.rightCropSpinButton = rightCropSpinButton
, GR.topCropSpinButton = topCropSpinButton
@@ -148,57 +149,58 @@ main = do
, GR.inFileChooserDialogCancelButton = inFileChooserDialogCancelButton
, GR.inFileChooserDialogOpenButton = inFileChooserDialogOpenButton
, GR.outFileChooserButton = outFileChooserButton
- , GR.fontChooserButton = fontChooserButton
+ , GR.textOverlaysAddButton = textOverlaysAddButton
, GR.saveButton = saveButton
, GR.openButton = openButton
- , GR.yesGtkButton = yesGtkButton
- , GR.noGtkButton = noGtkButton
+ , GR.confirmMessageDialogYesButton = confirmMessageDialogYesButton
+ , GR.confirmMessageDialogNoButton = confirmMessageDialogNoButton
, GR.aboutButton = aboutButton
, GR.giphyUploadButton = giphyUploadButton
, GR.imgurUploadButton = imgurUploadButton
, GR.saveAsVideoRadioButton = saveAsVideoRadioButton
- , GR.widthQualityPercentToggleButton = widthQualityPercentToggleButton
+ , GR.widthQualityToggleButton = widthQualityToggleButton
, GR.cropToggleButton = cropToggleButton
- , GR.topBottomTextToggleButton = topBottomTextToggleButton
+ , GR.textOverlaysToggleButton = textOverlaysToggleButton
, GR.saveOpenToggleButton = saveOpenToggleButton
, GR.uploadToggleButton = uploadToggleButton
+ , GR.videoPreviewPauseToggleButton = videoPreviewPauseToggleButton
, GR.inFileChooserDialogLabel = inFileChooserDialogLabel
, GR.inFileChooserButtonLabel = inFileChooserButtonLabel
, GR.startTimeAdjustment = startTimeAdjustment
, GR.durationTimeAdjustment = durationTimeAdjustment
, GR.widthSizeAdjustment = widthSizeAdjustment
- , GR.qualityPercentAdjustment = qualityPercentAdjustment
, GR.outFileNameEntry = outFileNameEntry
- , GR.topTextEntry = topTextEntry
- , GR.bottomTextEntry = bottomTextEntry
, GR.statusEntry = statusEntry
+ , GR.sidebarControlsPreviewbox = sidebarControlsPreviewbox
, GR.mainPreviewBox = mainPreviewBox
, GR.imagesPreviewBox = imagesPreviewBox
, GR.videoPreviewBox = videoPreviewBox
, GR.videoPreviewOverlayChildBox = videoPreviewOverlayChildBox
- , GR.widthQualityPercentBox = widthQualityPercentBox
+ , GR.widthQualityBox = widthQualityBox
, GR.cropSpinButtonsBox = cropSpinButtonsBox
- , GR.topBottomTextFontChooserBox = topBottomTextFontChooserBox
+ , GR.textOverlaysMainBox = textOverlaysMainBox
+ , GR.textOverlaysBox = textOverlaysBox
, GR.saveOpenBox = saveOpenBox
, GR.uploadBox = uploadBox
+ , GR.qualityComboBoxText = qualityComboBoxText
, GR.videoPreviewDrawingArea = videoPreviewDrawingArea
+ , GR.timeSlicesDrawingArea = timeSlicesDrawingArea
, GR.firstFramePreviewImageDrawingArea = firstFramePreviewImageDrawingArea
, GR.lastFramePreviewImageDrawingArea = lastFramePreviewImageDrawingArea
, GR.inFileChooserButtonImage = inFileChooserButtonImage
, GR.firstFrameImage = firstFrameImage
, GR.lastFrameImage = lastFrameImage
, GR.inFileChooserDialog = inFileChooserDialog
- , GR.longGifGtkMessageDialog = longGifGtkMessageDialog
+ , GR.confirmMessageDialog = confirmMessageDialog
, GR.aboutDialog = aboutDialog
- , GR.startTimeProgressBar = startTimeProgressBar
- , GR.endTimeProgressBar = endTimeProgressBar
, GR.saveSpinner = saveSpinner
, GR.inFileChooserWidget = inFileChooserWidget
, GR.maybeVideoPreviewWidget = maybeVideoPreviewWidget
, GR.maybePlaybinElement = maybePlaybinElement
, GR.temporaryDirectory = temporaryDirectory
- , GR.guiPreviewStateRef = guiPreviewStateRef
, GR.inVideoPropertiesRef = inVideoPropertiesRef
+ , GR.textOverlaysRef = textOverlaysRef
+ , GR.guiPreviewStateRef = guiPreviewStateRef
}
_ <- hideWidgetsOnRealize guiComponents
@@ -212,6 +214,8 @@ main = do
_ <- handleWindow guiComponents
_ <- handleGuiPreview guiComponents
+ GuiTextOverlays.handleTextOverlaysAddButton guiComponents
+
GuiStyle.applyCss guiComponents
GuiCapabilities.checkCapabilitiesAndNotify guiComponents
@@ -229,23 +233,24 @@ builderGetObject
-> a
-> String
-> IO b
-builderGetObject objectTypeClass builder objectId =
- fromJust <$> GI.Gtk.builderGetObject builder (pack objectId) >>=
- GI.Gtk.unsafeCastTo objectTypeClass
+builderGetObject objectTypeClass builder objectId = do
+ maybeObject <- GI.Gtk.builderGetObject builder $ pack objectId
+ when (isNothing maybeObject) $
+ putStrLn $ "[ERROR] could not build " ++ objectId ++ "."
+ GI.Gtk.unsafeCastTo objectTypeClass $ fromJust maybeObject
handleFileChooserDialogReponse :: GR.GuiComponents -> Int32 -> IO ()
handleFileChooserDialogReponse
guiComponents@GR.GuiComponents
- { GR.startTimeSpinButton
+ { GR.sidebarControlsPreviewbox
+ , GR.startTimeSpinButton
, GR.durationTimeSpinButton
, GR.leftCropSpinButton
, GR.rightCropSpinButton
, GR.topCropSpinButton
, GR.bottomCropSpinButton
, GR.widthSizeSpinButton
- , GR.qualityPercentSpinButton
- , GR.topTextEntry
- , GR.bottomTextEntry
+ , GR.qualityComboBoxText
, GR.outFileNameEntry
, GR.inFileChooserDialog
, GR.statusEntry
@@ -275,15 +280,15 @@ handleFileChooserDialogReponse
, GR.inVideoWidth = width
, GR.inVideoHeight = height
}
- let videoDuration = float2Double videoDuration'
+ let videoDuration = floatToDouble videoDuration'
let startTimeFraction = 0.25
- let startTime = videoDuration * startTimeFraction
- let endTime = startTime * 3
- let durationTime = endTime - startTime
+ let startTime = videoDuration * startTimeFraction
+ let endTime = startTime * 3
+ let durationTime = endTime - startTime
let videoDurationText = Data.Text.pack $ printf "%.3f" videoDuration
_ <- updateStartAndDurationTimeSpinButtonRanges guiComponents
- _ <- GI.Gtk.spinButtonSetValue startTimeSpinButton startTime
- _ <- GI.Gtk.spinButtonSetValue durationTimeSpinButton durationTime
+ _ <- GI.Gtk.spinButtonSetValue startTimeSpinButton $ truncatePastDigit startTime 2
+ _ <- GI.Gtk.spinButtonSetValue durationTimeSpinButton $ truncatePastDigit durationTime 2
_ <- updateStatusEntryAsync statusEntry 1 $
Data.Text.concat
[ "That video is about "
@@ -293,9 +298,11 @@ handleFileChooserDialogReponse
GI.Gtk.labelSetText inFileChooserButtonLabel $
Data.Text.pack $
takeFileName inFilePath
+ _ <- GI.Gtk.widgetShow sidebarControlsPreviewbox
return ()
_ -> do
atomicWriteIORef inVideoPropertiesRef GR.defaultInVideoProperties
+ _ <- GI.Gtk.widgetHide sidebarControlsPreviewbox
_ <- updateStartAndDurationTimeSpinButtonRanges guiComponents
_ <- GI.Gtk.spinButtonSetValue startTimeSpinButton 0.0
_ <- GI.Gtk.spinButtonSetValue durationTimeSpinButton 0.0
@@ -304,23 +311,22 @@ handleFileChooserDialogReponse
return ()
syncStartAndDurationTimeSpinButtons guiComponents
resetTextEntries
- resetWidthAndQualityPercentSpinButtons
+ resetWidthAndQuality
resetCropSpinButtons
+ GuiTextOverlays.removeTextOverlays guiComponents
where
resetTextEntries :: IO ()
resetTextEntries = do
let textEntries =
- [ topTextEntry
- , bottomTextEntry
- , outFileNameEntry
+ [ outFileNameEntry
]
mapM_
- (flip GI.Gtk.entrySetText "")
+ (`GI.Gtk.entrySetText` "")
textEntries
- resetWidthAndQualityPercentSpinButtons :: IO ()
- resetWidthAndQualityPercentSpinButtons = do
- GI.Gtk.spinButtonSetValue widthSizeSpinButton 500
- GI.Gtk.spinButtonSetValue qualityPercentSpinButton 100
+ resetWidthAndQuality :: IO ()
+ resetWidthAndQuality = do
+ GI.Gtk.spinButtonSetValue widthSizeSpinButton 500
+ GI.Gtk.setComboBoxActiveId qualityComboBoxText "Medium"
resetCropSpinButtons :: IO ()
resetCropSpinButtons = do
let spinButtons =
@@ -330,7 +336,7 @@ handleFileChooserDialogReponse
, bottomCropSpinButton
]
mapM_
- (flip GI.Gtk.spinButtonSetValue 0.0)
+ (`GI.Gtk.spinButtonSetValue` 0.0)
spinButtons
handleSpinButtons :: GR.GuiComponents -> IO ()
@@ -339,7 +345,6 @@ handleSpinButtons
{ GR.startTimeSpinButton
, GR.durationTimeSpinButton
, GR.widthSizeSpinButton
- , GR.qualityPercentSpinButton
, GR.leftCropSpinButton
, GR.rightCropSpinButton
, GR.topCropSpinButton
@@ -357,9 +362,6 @@ handleSpinButtons
_ <- GI.Gtk.onSpinButtonValueChanged
widthSizeSpinButton
handleWidthSizeSpinButton
- _ <- GI.Gtk.onSpinButtonValueChanged
- qualityPercentSpinButton
- handleQualityPercentSpinButton
_ <- GI.Gtk.onSpinButtonValueChanged
leftCropSpinButton
(handleCropSpinButton leftCropSpinButton rightCropSpinButton "left")
@@ -395,7 +397,8 @@ handleSpinButtons
_ <- setSpinButtonFraction durationTimeSpinButton
if
durationTime < 0.0
- || durationTime > (videoDuration - startTime)
+ -- 2.1 > 10.2 - 8.1
+ || (10.0 * durationTime) > ((10.0 * videoDuration) - (10.0 * startTime))
|| durationTime > videoDuration
then do
GI.Gtk.entrySetText statusEntry "The duration time is wrong."
@@ -415,22 +418,11 @@ handleSpinButtons
else do
GI.Gtk.entrySetText statusEntry "Ready."
unhighlightSpinButton widthSizeSpinButton
- handleQualityPercentSpinButton :: IO ()
- handleQualityPercentSpinButton = do
- qualityPercent <- double2Float <$> GI.Gtk.spinButtonGetValue qualityPercentSpinButton
- _ <- setSpinButtonFraction qualityPercentSpinButton
- if qualityPercent <= 0.0 || qualityPercent > 100.0
- then do
- GI.Gtk.entrySetText statusEntry "The quality percent is wrong."
- highlightSpinButton qualityPercentSpinButton
- else do
- GI.Gtk.entrySetText statusEntry "Ready."
- unhighlightSpinButton qualityPercentSpinButton
handleCropSpinButton :: GI.Gtk.SpinButton -> GI.Gtk.SpinButton -> Text -> IO ()
handleCropSpinButton a b t = do
cropValue <- double2Float <$> GI.Gtk.spinButtonGetValue a
_ <- setSpinButtonFraction a
- if cropValue < 0.0 || cropValue > 100.0
+ if cropValue < 0.0 || cropValue >= 1.0
then do
GI.Gtk.entrySetText statusEntry $ Data.Text.concat ["The ", t, " crop is wrong."]
highlightSpinButton a
@@ -442,8 +434,8 @@ handleSpinButtons
syncCropSpinButtons a b = do
aValue <- GI.Gtk.spinButtonGetValue a
bValue <- GI.Gtk.spinButtonGetValue b
- when (aValue + bValue >= 100) $ do
- let newValue = 100.0 - aValue - 1.0
+ when (aValue + bValue >= 1) $ do
+ let newValue = 1.0 - aValue - 0.01
let newValue' = if newValue < 0.0 then 0.0 else newValue
void $ GI.Gtk.spinButtonSetValue b newValue'
return ()
@@ -453,44 +445,41 @@ handleSpinButtons
handleSaveButtonClick :: GR.GuiComponents -> IO ()
handleSaveButtonClick
- GR.GuiComponents
+ guiComponents@GR.GuiComponents
{ GR.outFileChooserButton
, GR.outFileNameEntry
, GR.saveButton
, GR.openButton
- , GR.fontChooserButton
, GR.saveAsVideoRadioButton
, GR.startTimeSpinButton
, GR.durationTimeSpinButton
, GR.widthSizeSpinButton
- , GR.qualityPercentSpinButton
+ , GR.qualityComboBoxText
, GR.leftCropSpinButton
, GR.rightCropSpinButton
, GR.topCropSpinButton
, GR.bottomCropSpinButton
- , GR.topTextEntry
- , GR.bottomTextEntry
, GR.statusEntry
- , GR.inFileChooserWidget
- , GR.longGifGtkMessageDialog
+ , GR.confirmMessageDialog
, GR.saveSpinner
+ , GR.inVideoPropertiesRef
}
=
void $ GI.Gtk.onWidgetButtonReleaseEvent saveButton $ \ _ -> do
- inFilePath <- fileChooserGetFilePath inFileChooserWidget
+ GR.InVideoProperties
+ { GR.inVideoUri = inFilePath
+ } <- readIORef inVideoPropertiesRef
startTime <- double2Float <$> GI.Gtk.spinButtonGetValue startTimeSpinButton
durationTime <- double2Float <$> GI.Gtk.spinButtonGetValue durationTimeSpinButton
widthSize <- double2Float <$> GI.Gtk.spinButtonGetValue widthSizeSpinButton
- qualityPercent <- double2Float <$> GI.Gtk.spinButtonGetValue qualityPercentSpinButton
+ quality <- getQuality
leftCrop <- double2Float <$> GI.Gtk.spinButtonGetValue leftCropSpinButton
rightCrop <- double2Float <$> GI.Gtk.spinButtonGetValue rightCropSpinButton
topCrop <- double2Float <$> GI.Gtk.spinButtonGetValue topCropSpinButton
bottomCrop <- double2Float <$> GI.Gtk.spinButtonGetValue bottomCropSpinButton
- fontChoice <- GI.Gtk.fontButtonGetFontName fontChooserButton
- topText <- GI.Gtk.entryGetText topTextEntry
- bottomText <- GI.Gtk.entryGetText bottomTextEntry
saveAsVideo <- GI.Gtk.toggleButtonGetActive saveAsVideoRadioButton
outFilePath <- outFileChooserButtonGetFilePath outFileChooserButton outFileNameEntry
+ textOverlays <- GuiTextOverlays.getGifcurryTextOverlays guiComponents
let params =
Gifcurry.defaultGifParams
{ Gifcurry.inputFile = inFilePath
@@ -499,24 +488,25 @@ handleSaveButtonClick
, Gifcurry.startTime = startTime
, Gifcurry.durationTime = durationTime
, Gifcurry.widthSize = truncate widthSize
- , Gifcurry.qualityPercent = qualityPercent
- , Gifcurry.fontChoice = unpack fontChoice
- , Gifcurry.topText = unpack topText
- , Gifcurry.bottomText = unpack bottomText
+ , Gifcurry.quality = quality
, Gifcurry.leftCrop = leftCrop
, Gifcurry.rightCrop = rightCrop
, Gifcurry.topCrop = topCrop
, Gifcurry.bottomCrop = bottomCrop
+ , Gifcurry.textOverlays = textOverlays
}
paramsValid <- Gifcurry.gifParamsValid params
GI.Gtk.entrySetText statusEntry "Ready."
if paramsValid
then do
- longGifGtkMessageDialogResponse <-
+ GI.Gtk.setMessageDialogText
+ confirmMessageDialog
+ "Create a GIF with that long of a duration?"
+ confirmMessageDialogResponse <-
if durationTime >= durationTimeWarningLevel
- then GI.Gtk.dialogRun longGifGtkMessageDialog
+ then GI.Gtk.dialogRun confirmMessageDialog
else return (enumToInt32 GI.Gtk.ResponseTypeYes)
- when (longGifGtkMessageDialogResponse == enumToInt32 GI.Gtk.ResponseTypeYes) $ do
+ when (confirmMessageDialogResponse == enumToInt32 GI.Gtk.ResponseTypeYes) $ do
GI.Gtk.widgetSetSensitive saveButton False
GI.Gtk.widgetSetSensitive openButton False
GI.Gtk.widgetHide saveButton
@@ -544,6 +534,14 @@ handleSaveButtonClick
GI.Gtk.widgetSetSensitive openButton True
else GI.Gtk.entrySetText statusEntry "The settings are wrong."
return True
+ where
+ getQuality :: IO Gifcurry.Quality
+ getQuality =
+ fromMaybe Gifcurry.QualityMedium .
+ Gifcurry.qualityFromString .
+ Data.Text.unpack .
+ fromMaybe "Medium" <$>
+ GI.Gtk.getComboBoxActiveId qualityComboBoxText
handleOpenButtonClick :: GR.GuiComponents -> IO ()
handleOpenButtonClick
@@ -578,7 +576,7 @@ handleDialogs
guiComponents@GR.GuiComponents
{ GR.inFileChooserDialog
, GR.aboutDialog
- , GR.longGifGtkMessageDialog
+ , GR.confirmMessageDialog
, GR.aboutButton
, GR.inFileChooserButton
}
@@ -587,8 +585,8 @@ handleDialogs
aboutButton
(\ _ -> GI.Gtk.dialogRun aboutDialog >> return True)
_ <- GI.Gtk.onDialogResponse
- longGifGtkMessageDialog
- (\ _ -> GI.Gtk.widgetHide longGifGtkMessageDialog)
+ confirmMessageDialog
+ (\ _ -> GI.Gtk.widgetHide confirmMessageDialog)
_ <- GI.Gtk.onDialogResponse
aboutDialog
(\ _ -> GI.Gtk.widgetHide aboutDialog)
@@ -606,29 +604,29 @@ handleDialogs
handleSidebarSectionToggleButtons :: GR.GuiComponents -> IO ()
handleSidebarSectionToggleButtons
GR.GuiComponents
- { GR.widthQualityPercentToggleButton
+ { GR.widthQualityToggleButton
, GR.cropToggleButton
- , GR.topBottomTextToggleButton
+ , GR.textOverlaysToggleButton
, GR.saveOpenToggleButton
, GR.uploadToggleButton
- , GR.widthQualityPercentBox
+ , GR.widthQualityBox
, GR.cropSpinButtonsBox
- , GR.topBottomTextFontChooserBox
+ , GR.textOverlaysMainBox
, GR.saveOpenBox
, GR.uploadBox
}
= do
let toggleButtons =
- [ widthQualityPercentToggleButton
+ [ widthQualityToggleButton
, cropToggleButton
- , topBottomTextToggleButton
+ , textOverlaysToggleButton
, saveOpenToggleButton
, uploadToggleButton
]
let boxes =
- [ widthQualityPercentBox
+ [ widthQualityBox
, cropSpinButtonsBox
- , topBottomTextFontChooserBox
+ , textOverlaysMainBox
, saveOpenBox
, uploadBox
]
@@ -765,17 +763,20 @@ unhighlightSpinButton :: GI.Gtk.SpinButton -> IO ()
unhighlightSpinButton = styleSpinButtonAndEntry "{}"
styleSpinButtonAndEntry :: String -> GI.Gtk.SpinButton -> IO ()
-styleSpinButtonAndEntry style =
+styleSpinButtonAndEntry style spinButton = do
+ name <- Data.Text.unpack . Data.Text.strip <$> GI.Gtk.widgetGetName spinButton
+ let name' = if Data.List.null name then "" else "#" ++ name
GuiStyle.styleWidget
- ( "spinbutton "
- ++ style
- ++ " .spinbutton "
- ++ style
- ++ " spinbutton entry "
+ ( "spinbutton"
+ ++ name'
+ ++ " entry "
++ style
- ++ " .spinbutton .entry "
+ ++ " GtkSpinButton"
+ ++ name'
+ ++ " GtkEntry "
++ style
)
+ spinButton
updateStatusEntryAsync :: GI.Gtk.Entry -> Word32 -> Text -> IO ()
updateStatusEntryAsync statusEntry seconds message =
@@ -791,7 +792,7 @@ setSpinButtonFraction spinButton = do
(_, maxValue) <- GI.Gtk.spinButtonGetRange spinButton
value <- GI.Gtk.spinButtonGetValue spinButton
let fraction = if maxValue <= 0.0 then 0.0 else abs $ value / maxValue
- void $ GI.Gtk.setEntryProgressFraction spinButton fraction
+ void $ GI.Gtk.setEntryProgressFraction spinButton $ truncatePastDigit fraction 2
updateStartAndDurationTimeSpinButtonRanges :: GR.GuiComponents -> IO ()
updateStartAndDurationTimeSpinButtonRanges
@@ -801,21 +802,13 @@ updateStartAndDurationTimeSpinButtonRanges
, GR.inVideoPropertiesRef
}
= do
- videoDuration <- float2Double . GR.inVideoDuration <$> readIORef inVideoPropertiesRef
- let buffer = if videoDuration * 0.01 > 0.1 then 0.1 else videoDuration * 0.01
+ startTime <- GI.Gtk.spinButtonGetValue startTimeSpinButton
+ videoDuration <- floatToDouble . GR.inVideoDuration <$> readIORef inVideoPropertiesRef
+ let startTime' = if startTime >= videoDuration then videoDuration else startTime
+ let maxDurationTime = if videoDuration - startTime' <= 0.0 then 0.0 else videoDuration - startTime'
+ let buffer = if videoDuration * 0.01 > 0.1 then 0.1 else videoDuration * 0.01
_ <- GI.Gtk.spinButtonSetRange startTimeSpinButton 0.0 (videoDuration - buffer)
- _ <- GI.Gtk.spinButtonSetRange durationTimeSpinButton buffer videoDuration
- return ()
-
-updateStartAndDurationTimeSpinButtonFractions :: GR.GuiComponents -> IO ()
-updateStartAndDurationTimeSpinButtonFractions
- GR.GuiComponents
- { GR.startTimeSpinButton
- , GR.durationTimeSpinButton
- }
- = do
- _ <- setSpinButtonFraction startTimeSpinButton
- _ <- setSpinButtonFraction durationTimeSpinButton
+ _ <- GI.Gtk.spinButtonSetRange durationTimeSpinButton buffer maxDurationTime
return ()
syncStartAndDurationTimeSpinButtons :: GR.GuiComponents -> IO ()
@@ -826,57 +819,46 @@ syncStartAndDurationTimeSpinButtons
, GR.inVideoPropertiesRef
}
= do
- startTime <- GI.Gtk.spinButtonGetValue startTimeSpinButton
- durationTime <- GI.Gtk.spinButtonGetValue durationTimeSpinButton
- videoDuration <- float2Double . GR.inVideoDuration <$> readIORef inVideoPropertiesRef
- let startTime' = if startTime >= videoDuration then videoDuration else startTime
- let maxDurationTime =
- if videoDuration - startTime' <= 0.0 then 0.0 else videoDuration - startTime'
- let durationTime' =
- if durationTime >= maxDurationTime then maxDurationTime else durationTime
+ startTime <- GI.Gtk.spinButtonGetValue startTimeSpinButton
+ durationTime <- GI.Gtk.spinButtonGetValue durationTimeSpinButton
+ videoDuration <- floatToDouble . GR.inVideoDuration <$> readIORef inVideoPropertiesRef
+ let startTime' = if startTime >= videoDuration then videoDuration else startTime
+ let maxDurationTime = if videoDuration - startTime' <= 0.0 then 0.0 else videoDuration - startTime'
+ let durationTime' = if durationTime >= maxDurationTime then maxDurationTime else durationTime
_ <- updateStartAndDurationTimeSpinButtonRanges guiComponents
- _ <- GI.Gtk.spinButtonSetValue startTimeSpinButton startTime'
- _ <- GI.Gtk.spinButtonSetValue durationTimeSpinButton durationTime'
+ _ <- GI.Gtk.spinButtonSetValue startTimeSpinButton $ truncatePastDigit startTime' 2
+ _ <- GI.Gtk.spinButtonSetValue durationTimeSpinButton $ truncatePastDigit durationTime' 2
updateStartAndDurationTimeSpinButtonFractions guiComponents
- updateStartAndEndTimeProgressBars guiComponents
-updateStartAndEndTimeProgressBars :: GR.GuiComponents -> IO ()
-updateStartAndEndTimeProgressBars
+updateStartAndDurationTimeSpinButtonFractions :: GR.GuiComponents -> IO ()
+updateStartAndDurationTimeSpinButtonFractions
GR.GuiComponents
{ GR.startTimeSpinButton
, GR.durationTimeSpinButton
- , GR.startTimeProgressBar
- , GR.endTimeProgressBar
- , GR.inVideoPropertiesRef
}
= do
- videoDuration <- float2Double . GR.inVideoDuration <$> readIORef inVideoPropertiesRef
- startTime <- GuiMisc.clamp 0.0 videoDuration <$> GI.Gtk.spinButtonGetValue startTimeSpinButton
- durationTime <- GuiMisc.clamp 0.0 videoDuration <$> GI.Gtk.spinButtonGetValue durationTimeSpinButton
- let endTime = startTime + durationTime
- let endTime' = GuiMisc.clamp 0.0 videoDuration $ videoDuration - endTime
- let startTimeProgressBarFraction = fromMaybe 0.0 $ safeDivide startTime videoDuration
- let endTimeProgressBarFraction = fromMaybe 0.0 $ safeDivide endTime' videoDuration
- _ <- GI.Gtk.progressBarSetFraction startTimeProgressBar startTimeProgressBarFraction
- _ <- GI.Gtk.progressBarSetFraction endTimeProgressBar endTimeProgressBarFraction
+ _ <- setSpinButtonFraction startTimeSpinButton
+ _ <- setSpinButtonFraction durationTimeSpinButton
return ()
hideWidgetsOnRealize :: GR.GuiComponents -> IO ()
hideWidgetsOnRealize
GR.GuiComponents
{ GR.saveSpinner
- , GR.widthQualityPercentBox
+ , GR.sidebarControlsPreviewbox
+ , GR.widthQualityBox
, GR.cropSpinButtonsBox
- , GR.topBottomTextFontChooserBox
+ , GR.textOverlaysMainBox
, GR.saveOpenBox
, GR.uploadBox
}
= do
hideOnRealize saveSpinner
let boxes =
- [ widthQualityPercentBox
+ [ sidebarControlsPreviewbox
+ , widthQualityBox
, cropSpinButtonsBox
- , topBottomTextFontChooserBox
+ , textOverlaysMainBox
, saveOpenBox
, uploadBox
]
diff --git a/src/lib/Gifcurry.hs b/src/lib/Gifcurry.hs
index 11931e3..d19e8ef 100644
--- a/src/lib/Gifcurry.hs
+++ b/src/lib/Gifcurry.hs
@@ -14,14 +14,18 @@
module Gifcurry
( gif
, GifParams(..)
+ , Quality(..)
+ , TextOverlay(..)
+ , TextOverlayOrigin(..)
, defaultGifParams
- , defaultFontChoice
, gifParamsValid
, versionNumber
, getVideoDurationInSeconds
, getOutputFileWithExtension
, getVideoWidthAndHeight
, findOrCreateTemporaryDirectory
+ , qualityFromString
+ , textOverlayOriginFromString
)
where
@@ -29,57 +33,122 @@ import System.Process
import System.IO.Temp
import System.Directory
import System.FilePath
+import qualified System.FilePath.Find as SFF
+import Control.Exception
+import Control.Monad
import Text.Read
+import Text.ParserCombinators.ReadP
+import Text.Printf
import Data.Maybe
import Data.List
import Data.Text
import Data.Either
-import Text.Printf
-import Control.Exception
-import Control.Monad
-- | The data type record required by 'gif'.
data GifParams =
GifParams
- { inputFile :: String
- , outputFile :: String
- , saveAsVideo :: Bool
- , startTime :: Float
- , durationTime :: Float
- , widthSize :: Int
- , qualityPercent :: Float
- , fontChoice :: String
- , topText :: String
- , bottomText :: String
- , leftCrop :: Float
- , rightCrop :: Float
- , topCrop :: Float
- , bottomCrop :: Float
+ { inputFile :: String
+ , outputFile :: String
+ , saveAsVideo :: Bool
+ , startTime :: Float
+ , durationTime :: Float
+ , widthSize :: Int
+ , quality :: Quality
+ , textOverlays :: [TextOverlay]
+ , leftCrop :: Float
+ , rightCrop :: Float
+ , topCrop :: Float
+ , bottomCrop :: Float
}
deriving (Show, Read)
+-- | The data type that holds the needed information to render text on top of the GIF.
+data TextOverlay =
+ TextOverlay
+ { textOverlayText :: String
+ , textOverlayFontFamily :: String
+ , textOverlayFontStyle :: String
+ , textOverlayFontStretch :: String
+ , textOverlayFontWeight :: Int
+ , textOverlayFontSize :: Int
+ , textOverlayOrigin :: TextOverlayOrigin
+ , textOverlayXTranslation :: Float
+ , textOverlayYTranslation :: Float
+ , textOverlayRotation :: Int
+ , textOverlayStartTime :: Float
+ , textOverlayDurationTime :: Float
+ , textOverlayOutlineSize :: Int
+ , textOverlayOutlineColor :: String
+ , textOverlayFillColor :: String
+ }
+ deriving (Show, Read)
+
+-- | The starting point for a text overlay.
+data TextOverlayOrigin =
+ TextOverlayOriginNorthWest
+ | TextOverlayOriginNorth
+ | TextOverlayOriginNorthEast
+ | TextOverlayOriginWest
+ | TextOverlayOriginCenter
+ | TextOverlayOriginEast
+ | TextOverlayOriginSouthWest
+ | TextOverlayOriginSouth
+ | TextOverlayOriginSouthEast
+ deriving (Read)
+
+instance Show TextOverlayOrigin where
+ show TextOverlayOriginNorthWest = "NorthWest"
+ show TextOverlayOriginNorth = "North"
+ show TextOverlayOriginNorthEast = "NorthEast"
+ show TextOverlayOriginWest = "West"
+ show TextOverlayOriginCenter = "Center"
+ show TextOverlayOriginEast = "East"
+ show TextOverlayOriginSouthWest = "SouthWest"
+ show TextOverlayOriginSouth = "South"
+ show TextOverlayOriginSouthEast = "SouthEast"
+
+-- | Controls the amount of colors used and the frame rate.
+-- Higher values will result in a larger file size.
+data Quality =
+ QualityHigh
+ | QualityMedium
+ | QualityLow
+ deriving (Read)
+
+instance Show Quality where
+ show QualityHigh = "High"
+ show QualityMedium = "Medium"
+ show QualityLow = "Low"
+
-- | The version number.
versionNumber :: String
-versionNumber = "3.0.0.2"
+versionNumber = "4.0.0.0"
--- | Specifies default parameters for 'startTime', 'durationTime', 'widthSize', 'qualityPercent', and 'fontChoice'.
+-- | Specifies the default parameters for the following.
+-- * 'startTime'
+-- * 'durationTime'
+-- * 'widthSize'
+-- * 'quality'
+-- * 'textOverlays'
+-- * 'leftCrop'
+-- * 'rightCrop'
+-- * 'topCrop'
+-- * 'bottomCrop'
defaultGifParams :: GifParams
defaultGifParams =
GifParams
- { inputFile = ""
- , outputFile = ""
- , saveAsVideo = False
- , startTime = 0.0
- , durationTime = 1.0
- , widthSize = 500
- , qualityPercent = 100.0
- , fontChoice = defaultFontChoice
- , topText = ""
- , bottomText = ""
- , leftCrop = 0.0
- , rightCrop = 0.0
- , topCrop = 0.0
- , bottomCrop = 0.0
+ { inputFile = ""
+ , outputFile = ""
+ , saveAsVideo = False
+ , startTime = 0.0
+ , durationTime = 1.0
+ , widthSize = 500
+ , quality = QualityHigh
+ , textOverlays = []
+ , leftCrop = 0.0
+ , rightCrop = 0.0
+ , topCrop = 0.0
+ , bottomCrop = 0.0
}
-- | Inputs 'GifParams' and outputs either an IO IOError or IO String.
@@ -98,66 +167,157 @@ defaultGifParams =
-- @
gif :: GifParams -> IO (Either IOError String)
gif
- gifParams@GifParams { saveAsVideo }
+ gifParams@GifParams
+ { widthSize
+ , saveAsVideo
+ , startTime
+ , textOverlays
+ , quality
+ }
= do
- temporaryDirectory <- findOrCreateTemporaryDirectory
- withTempDirectory temporaryDirectory "gifcurry-frames" $ \ tempDir ->
- handleFrameExtraction tempDir
- >>= handleFrameMerge tempDir
- >>= handleGifToVideoConversion
+ printGifParams gifParams
+ validParams <- gifParamsValid gifParams
+ if validParams
+ then do
+ temporaryDirectory <- findOrCreateTemporaryDirectory
+ withTempDirectory temporaryDirectory "gifcurry-frames" $ \ tempDir ->
+ handleFrameExtraction tempDir >>=
+ handleFrameAnnotations tempDir >>=
+ handleFrameMerge tempDir
+ else return $ Left $ userError "Invalid params."
where
handleFrameExtraction :: String -> IO (Either IOError Float)
handleFrameExtraction tempDir = do
- printGifParams gifParams
- validParams <- gifParamsValid gifParams
- if validParams
- then do
- frameRate <-
- validateAndAdjustFrameRate gifParams <$>
- getVideoAverageFrameRateInSeconds gifParams
- result <- extractFrames gifParams tempDir frameRate
- case result of
- Left x -> do
- putStrLn "[ERROR] Something went wrong with FFmpeg."
- return $ Left x
- Right _ -> return $ Right frameRate
- else return $ Left $ userError "Invalid params."
- handleFrameMerge :: String -> Either IOError Float -> IO (Either IOError String)
- handleFrameMerge tempDir (Right frameRate) = do
- fontMatch <- getFontMatch gifParams
- let gifParams' = gifParams { fontChoice = fontMatch }
- result <- mergeFramesIntoGif gifParams' tempDir frameRate
+ frameRate <- qualityAndFrameRateToFrameRate quality . fromMaybe defaultFrameRate <$>
+ getVideoAverageFrameRateInSeconds gifParams
+ result <- extractFrames gifParams tempDir frameRate
case result of
- Left x -> do
- putStrLn "[ERROR] Something went wrong with ImageMagick."
+ Left x -> do
+ putStrLn "[ERROR] Something went wrong with FFmpeg."
return $ Left x
- Right gifFilePath -> return $ Right gifFilePath
- handleFrameMerge _ (Left x) = return $ Left x
- handleGifToVideoConversion :: Either IOError String -> IO (Either IOError String)
- handleGifToVideoConversion (Right gifFilePath) =
+ Right _ -> return $ Right frameRate
+ handleFrameAnnotations :: String -> Either IOError Float -> IO (Either IOError Float)
+ handleFrameAnnotations tempDir (Right frameRate)
+ | Prelude.null textOverlays = return $ Right frameRate
+ | frameRate <= 0.0 = do
+ let errorString = "Frame rate is less than or equal to zero."
+ putStrLn $ "[ERROR] " ++ errorString
+ return $ Left $ userError errorString
+ | otherwise = do
+ frameFilePaths <-
+ SFF.find
+ SFF.always
+ (SFF.fileName SFF.~~? "*extracted-frames_*")
+ tempDir
+ let maybeFrameNumbers = getFrameNumbers frameFilePaths
+ case maybeFrameNumbers of
+ Just frameNumbers -> do
+ fontFamilies <- getFontFamilies
+ maybeInVideoWidthHeight <- getVideoWidthAndHeight gifParams
+ let frameSeconds =
+ Prelude.map
+ (\ x -> startTime + ((realToFrac x :: Float) * (1.0 / frameRate)))
+ frameNumbers
+ let frameFilePathsFrameSeconds = Prelude.zip frameFilePaths frameSeconds
+ let widthSize' = fromIntegral widthSize :: Float
+ let (gifWidthNoCrop, gifHeightNoCrop) =
+ case maybeInVideoWidthHeight of
+ Just (w, h) -> (widthSize', widthSize' * (h / w))
+ _ -> ( 0.0, 0.0)
+ putStrLn "[INFO] Adding text..."
+ results <-
+ mapM
+ (\ (filePath, second) -> do
+ let textOverlays' =
+ Prelude.foldl
+ (\ xs x ->
+ if textOverlayStartTime x <= second &&
+ textOverlayStartTime x + textOverlayDurationTime x >= second
+ then xs ++ [x]
+ else xs
+ )
+ []
+ textOverlays
+ annotateImage
+ gifParams
+ gifWidthNoCrop
+ gifHeightNoCrop
+ fontFamilies
+ filePath
+ textOverlays'
+ )
+ frameFilePathsFrameSeconds
+ if Prelude.any isLeft results
+ then
+ case results of
+ (Left x:_) -> return $ Left x
+ _ -> return $ Left $ userError "Could not annotate the frames."
+ else return $ Right frameRate
+ Nothing -> do
+ let errorString = "Could not find the frame numbers."
+ putStrLn $ "[ERROR] " ++ errorString
+ return $ Left $ userError errorString
+ handleFrameAnnotations _ (Left x) = return $ Left x
+ handleFrameMerge :: String -> Either IOError Float -> IO (Either IOError String)
+ handleFrameMerge tempDir (Right frameRate) =
if saveAsVideo
then do
- result <- convertGifToVideo gifParams gifFilePath
+ result <- mergeFramesIntoVideo gifParams tempDir frameRate
case result of
Left x -> do
putStrLn "[ERROR] Something went wrong with FFmpeg."
return $ Left x
- Right outputFileWithExtension -> do
+ Right videoFilePath -> do
putStrLn "[INFO] All done."
- return $ Right outputFileWithExtension
+ return $ Right videoFilePath
else do
- putStrLn "[INFO] All done."
- return $ Right gifFilePath
- handleGifToVideoConversion result@(Left _) = return result
- getFontMatch :: GifParams -> IO String
- getFontMatch GifParams { topText = "", bottomText = "" } = defaultFontMatch
- getFontMatch gifParams' = do
- fontNames <- getListOfFontNames
- let match = bestFontNameMatch (fontChoiceOrDefault gifParams') fontNames
- putStrLn $ "[INFO] Your font choice matched to \"" ++ match ++ "\"."
- return match
- defaultFontMatch :: IO String
- defaultFontMatch = return defaultFontChoice
+ result <- mergeFramesIntoGif gifParams tempDir frameRate
+ case result of
+ Left x -> do
+ putStrLn "[ERROR] Something went wrong with ImageMagick."
+ return $ Left x
+ Right gifFilePath -> do
+ putStrLn "[INFO] All done."
+ return $ Right gifFilePath
+ handleFrameMerge _ (Left x) = return $ Left x
+
+-- | Convenience function that attempts to turn a string into a 'TextOverlayOrigin'.
+-- @
+-- textOverlayOriginFromString " cEntEr " -- Just TextOverlayOriginCenter
+-- textOverlayOriginFromString "test" -- Nothing
+-- @
+textOverlayOriginFromString :: String -> Maybe Gifcurry.TextOverlayOrigin
+textOverlayOriginFromString origin =
+ textOverlayOriginFromString' $
+ stripAndLowerString origin
+ where
+ textOverlayOriginFromString' :: String -> Maybe TextOverlayOrigin
+ textOverlayOriginFromString' "northwest" = Just TextOverlayOriginNorthWest
+ textOverlayOriginFromString' "north" = Just TextOverlayOriginNorth
+ textOverlayOriginFromString' "northeast" = Just TextOverlayOriginNorthEast
+ textOverlayOriginFromString' "west" = Just TextOverlayOriginWest
+ textOverlayOriginFromString' "center" = Just TextOverlayOriginCenter
+ textOverlayOriginFromString' "east" = Just TextOverlayOriginEast
+ textOverlayOriginFromString' "southwest" = Just TextOverlayOriginSouthWest
+ textOverlayOriginFromString' "south" = Just TextOverlayOriginSouth
+ textOverlayOriginFromString' "southeast" = Just TextOverlayOriginSouthEast
+ textOverlayOriginFromString' _ = Nothing
+
+-- | Convenience function that attempts to turn a string into a 'Quality'.
+-- @
+-- qualityFromString " hIgH " -- Just QualityHigh
+-- qualityFromString "test" -- Nothing
+-- @
+qualityFromString :: String -> Maybe Quality
+qualityFromString quality =
+ qualityFromString' $
+ stripAndLowerString quality
+ where
+ qualityFromString' :: String -> Maybe Quality
+ qualityFromString' "high" = Just QualityHigh
+ qualityFromString' "medium" = Just QualityMedium
+ qualityFromString' "low" = Just QualityLow
+ qualityFromString' _ = Nothing
-- | Outputs `True` or `False` if the parameters in the `GifParams` record are valid.
gifParamsValid :: GifParams -> IO Bool
@@ -168,37 +328,41 @@ gifParamsValid
, startTime
, durationTime
, widthSize
- , qualityPercent
, leftCrop
, rightCrop
, topCrop
, bottomCrop
+ , textOverlays
}
= do
inputFileExists <-
case Prelude.length inputFile of
0 -> return False
_ -> doesFileExist inputFile
- let widthSize' = fromIntegral widthSize :: Float
- let outputFileValid = not $ Data.Text.null $ Data.Text.strip $ Data.Text.pack outputFile
- let startTimeValid = startTime >= 0.0
- let durationTimeValid = durationTime > 0.0
- let widthSizeValid = widthSize >= 1
- let qualityPercentValid = qualityPercent >= 1.0 && qualityPercent <= 100.0
- let leftCropValid = cropValid leftCrop
- let rightCropValid = cropValid rightCrop
- let topCropValid = cropValid topCrop
- let bottomCropValid = cropValid bottomCrop
- let leftRightCropValid = cropValid (leftCrop + rightCrop)
- let topBottomCropValid = cropValid (topCrop + bottomCrop)
+ let widthSize' = fromIntegral widthSize :: Float
+ let outputFileValid = not $ Data.Text.null $ Data.Text.strip $ Data.Text.pack outputFile
+ let startTimeValid = startTime >= 0.0
+ let durationTimeValid = durationTime > 0.0
+ let widthSizeValid = widthSize >= 1
+ let leftCropValid = cropValid leftCrop
+ let rightCropValid = cropValid rightCrop
+ let topCropValid = cropValid topCrop
+ let bottomCropValid = cropValid bottomCrop
+ let leftRightCropValid = cropValid (leftCrop + rightCrop)
+ let topBottomCropValid = cropValid (topCrop + bottomCrop)
let widthLeftRightCropSizeValid =
- (widthSize' - (widthSize' * (leftCrop / 100.0)) - (widthSize' * (rightCrop / 100.0))) >= 1.0
+ (widthSize' - (widthSize' * leftCrop) - (widthSize' * rightCrop)) >= 1.0
+ let textOverlayColorsValid =
+ Prelude.all
+ (\ TextOverlay { textOverlayOutlineColor, textOverlayFillColor } ->
+ isJust (getRgb textOverlayOutlineColor) && isJust (getRgb textOverlayFillColor)
+ )
+ textOverlays
unless inputFileExists $ printError "Input video file does not exist."
unless outputFileValid $ printInvalid "Output File"
unless startTimeValid $ printInvalid "Start Time"
unless durationTimeValid $ printInvalid "Duration Time"
unless widthSizeValid $ printInvalid "Width Size"
- unless qualityPercentValid $ printInvalid "Quality Percent"
unless leftCropValid $ printInvalid "Left Crop"
unless rightCropValid $ printInvalid "Right Crop"
unless topCropValid $ printInvalid "Top Crop"
@@ -206,22 +370,22 @@ gifParamsValid
unless leftRightCropValid $ printInvalid "Left and Right Crop"
unless topBottomCropValid $ printInvalid "Top and Bottom Crop"
unless widthLeftRightCropSizeValid $ printError "Width Size too small with Left and Right Crop."
- let valid =
- inputFileExists
- && outputFileValid
- && startTimeValid
- && durationTimeValid
- && widthSizeValid
- && qualityPercentValid
- && leftCropValid
- && rightCropValid
- && topCropValid
- && bottomCropValid
- && widthLeftRightCropSizeValid
- return valid
+ unless textOverlayColorsValid $ printError "Text overlay color(s) invalid. The format is: rgb(r,g,b)"
+ return $
+ inputFileExists
+ && outputFileValid
+ && startTimeValid
+ && durationTimeValid
+ && widthSizeValid
+ && leftCropValid
+ && rightCropValid
+ && topCropValid
+ && bottomCropValid
+ && widthLeftRightCropSizeValid
+ && textOverlayColorsValid
where
cropValid :: Float -> Bool
- cropValid c = c >= 0.0 && c <= 100.0
+ cropValid c = c >= 0.0 && c < 1.0
printInvalid :: String -> IO ()
printInvalid s = printError $ s ++ " is invalid."
printError :: String -> IO ()
@@ -340,10 +504,6 @@ getOutputFileWithExtension GifParams { outputFile, saveAsVideo } =
++ "."
++ (if saveAsVideo then videoExtension else gifExtension)
--- | Returns the default font choice used if no font choice is specified.
-defaultFontChoice :: String
-defaultFontChoice = "sans-serif"
-
gifOutputFile :: String -> String
gifOutputFile outputFile =
getOutputFileWithExtension $
@@ -354,65 +514,6 @@ videoOutputFile outputFile =
getOutputFileWithExtension $
defaultGifParams { outputFile = outputFile, saveAsVideo = True }
-defaultFrameRate :: Float
-defaultFrameRate = 15.0
-
-validateAndAdjustFrameRate :: GifParams -> Maybe Float -> Float
-validateAndAdjustFrameRate gifParams =
- frameRateBasedOnQualityPercent gifParams . maybeFrameRateOrDefaultFrameRate
-
-maybeFrameRateOrDefaultFrameRate :: Maybe Float -> Float
-maybeFrameRateOrDefaultFrameRate (Just frameRate) =
- if frameRate <= defaultFrameRate then defaultFrameRate else frameRate
-maybeFrameRateOrDefaultFrameRate Nothing = defaultFrameRate
-
-frameRateBasedOnQualityPercent :: GifParams -> Float -> Float
-frameRateBasedOnQualityPercent GifParams { qualityPercent } frameRate =
- if result <= defaultFrameRate then defaultFrameRate else result
- where
- result :: Float
- result = frameRate * (qualityPercent / 100.0)
-
-getVideoAverageFrameRateInSeconds :: GifParams -> IO (Maybe Float)
-getVideoAverageFrameRateInSeconds GifParams { inputFile } = tryFfprobe params >>= result
- where
- result :: Either IOError String -> IO (Maybe Float)
- result (Left _) = return Nothing
- result (Right avgFrameRateString) = return $ processString avgFrameRateString
- where
- processString :: String -> Maybe Float
- processString =
- divideMaybeFloats . textsToMaybeFloats . filterNullTexts . splitText . cleanString
- cleanString :: String -> Text
- cleanString = Data.Text.strip . Data.Text.pack
- splitText :: Text -> [Text]
- splitText = Data.Text.split (== '/')
- filterNullTexts :: [Text] -> [Text]
- filterNullTexts = Data.List.filter (not . Data.Text.null)
- textsToMaybeFloats :: [Text] -> [Maybe Float]
- textsToMaybeFloats =
- Data.List.filter isJust
- . Data.List.map (\ s -> readMaybe (Data.Text.unpack s) :: Maybe Float)
- divideMaybeFloats :: [Maybe Float] -> Maybe Float
- divideMaybeFloats (Just n:Just d:_) =
- if d <= 0 || n <= 0 then Nothing else Just $ n / d
- divideMaybeFloats _ = Nothing
- params :: [String]
- params =
- [ "-v"
- , "error"
- , "-select_streams"
- , "v:0"
- , "-show_entries"
- , "stream=avg_frame_rate"
- , "-of"
- , "default=noprint_wrappers=1:nokey=1"
- , inputFile
- ]
-
-tryFfprobe :: [String] -> IO (Either IOError String)
-tryFfprobe params = try $ readProcess "ffprobe" params []
-
printGifParams :: GifParams -> IO ()
printGifParams
gifParams@GifParams
@@ -421,40 +522,88 @@ printGifParams
, startTime
, durationTime
, widthSize
- , qualityPercent
- , fontChoice
- , topText
- , bottomText
+ , quality
, leftCrop
, rightCrop
, topCrop
, bottomCrop
+ , textOverlays
}
=
putStrLn $
- Prelude.unlines
- [ "[INFO] Here are your settings."
- , ""
- , " - FILE IO:"
- , " - Input File: " ++ inputFile
- , " - Output File: " ++ getOutputFileWithExtension gifParams
- , " - Save As Video: " ++ if saveAsVideo then "Yes" else "No"
- , " - TIME:"
- , " - Start Second: " ++ printFloat startTime
- , " - Duration Time: " ++ printFloat durationTime ++ " seconds"
- , " - OUTPUT FILE SIZE:"
- , " - Width Size: " ++ show widthSize ++ "px"
- , " - Quality Percent: " ++ show (qualityPercentClamp qualityPercent) ++ "%"
- , " - TEXT:"
- , " - Font Choice: " ++ fontChoice
- , " - Top Text: " ++ topText
- , " - Bottom Text: " ++ bottomText
- , " - CROP:"
- , " - Left Crop: " ++ printFloat leftCrop
- , " - Right crop: " ++ printFloat rightCrop
- , " - Top Crop: " ++ printFloat topCrop
- , " - Bottom Crop: " ++ printFloat bottomCrop
- ]
+ Prelude.unlines $
+ [ "[INFO] Here are your settings."
+ , ""
+ , " - FILE IO:"
+ , " - Input File: " ++ inputFile
+ , " - Output File: " ++ getOutputFileWithExtension gifParams
+ , " - Save As Video: " ++ if saveAsVideo then "Yes" else "No"
+ , " - TIME:"
+ , " - Start Second: " ++ printFloat startTime
+ , " - Duration Time: " ++ printFloat durationTime ++ " seconds"
+ , " - OUTPUT FILE SIZE:"
+ , " - Width Size: " ++ show widthSize ++ "px"
+ , " - Quality: " ++ show quality
+ ]
+ ++ if Prelude.null textOverlays
+ then []
+ else
+ [ " - TEXT:"
+ ]
+ ++
+ Prelude.foldl
+ (\ xs
+ TextOverlay
+ { textOverlayText
+ , textOverlayFontFamily
+ , textOverlayFontStyle
+ , textOverlayFontStretch
+ , textOverlayFontWeight
+ , textOverlayFontSize
+ , textOverlayStartTime
+ , textOverlayDurationTime
+ , textOverlayOrigin
+ , textOverlayXTranslation
+ , textOverlayYTranslation
+ , textOverlayRotation
+ , textOverlayOutlineSize
+ , textOverlayOutlineColor
+ , textOverlayFillColor
+ }
+ ->
+ xs
+ ++ [ " - Text: " ++ textOverlayText
+ , " - Font:"
+ , " - Family: " ++ textOverlayFontFamily
+ , " - Size: " ++ show textOverlayFontSize
+ , " - Style: " ++ textOverlayFontStyle
+ , " - Stretch: " ++ textOverlayFontStretch
+ , " - Weight: " ++ show textOverlayFontWeight
+ , " - Time:"
+ , " - Start: " ++ printFloat textOverlayStartTime ++ " seconds"
+ , " - Duration: " ++ printFloat textOverlayDurationTime ++ " seconds"
+ , " - Translation:"
+ , " - Origin: " ++ show textOverlayOrigin
+ , " - X: " ++ show textOverlayXTranslation
+ , " - Y: " ++ show textOverlayYTranslation
+ , " - Rotation:"
+ , " - Degrees: " ++ show textOverlayRotation
+ , " - Outline: "
+ , " - Size: " ++ show textOverlayOutlineSize
+ , " - Color: " ++ textOverlayOutlineColor
+ , " - Fill:"
+ , " - Color: " ++ textOverlayFillColor
+ ]
+ )
+ []
+ textOverlays
+ ++
+ [ " - CROP:"
+ , " - Left: " ++ printFloat leftCrop
+ , " - Right: " ++ printFloat rightCrop
+ , " - Top: " ++ printFloat topCrop
+ , " - Bottom: " ++ printFloat bottomCrop
+ ]
where
printFloat :: Float -> String
printFloat = printf "%.3f"
@@ -493,7 +642,7 @@ extractFrames
widthSize' :: String
widthSize' = show widthSize
frameRate' :: String
- frameRate' = show $ maybeFrameRateOrDefaultFrameRate (Just frameRate)
+ frameRate' = show frameRate
params :: [String]
params =
[ "-nostats"
@@ -515,83 +664,244 @@ extractFrames
++ widthSize'
++ ":-1"
++",crop=w=iw*(1-"
- ++ show ((leftCrop + rightCrop) / 100.0)
+ ++ show (leftCrop + rightCrop)
++ "):h=ih*(1-"
- ++ show ((topCrop + bottomCrop) / 100.0)
+ ++ show (topCrop + bottomCrop)
++ "):x=iw*"
- ++ show (leftCrop / 100.0)
+ ++ show leftCrop
++ ":y=ih*"
- ++ show (topCrop / 100.0)
+ ++ show topCrop
++ ":exact=1"
+ , "-start_number"
+ , "0"
, "-f"
, "image2"
- , tempDir ++ "/%010d." ++ frameFileExtension
+ , tempDir ++ "/extracted-frames_%010d." ++ frameFileExtension
]
+annotateImage
+ :: GifParams
+ -> Float
+ -> Float
+ -> [Text]
+ -> String
+ -> [TextOverlay]
+ -> IO (Either IOError String)
+annotateImage
+ GifParams
+ { leftCrop
+ , rightCrop
+ , topCrop
+ , bottomCrop
+ }
+ gifWidthNoCrop
+ gifHeightNoCrop
+ fontFamilies
+ filePath
+ textOverlays
+ = do
+ let annotations =
+ Prelude.foldl
+ (\ xs
+ TextOverlay
+ { textOverlayText
+ , textOverlayFontFamily
+ , textOverlayFontStyle
+ , textOverlayFontStretch
+ , textOverlayFontWeight
+ , textOverlayFontSize
+ , textOverlayOrigin
+ , textOverlayXTranslation
+ , textOverlayYTranslation
+ , textOverlayRotation
+ , textOverlayOutlineSize
+ , textOverlayOutlineColor
+ , textOverlayFillColor
+ }
+ ->
+ xs
+ ++ fontFamilyArg fontFamilies textOverlayFontFamily
+ ++ [ "-style"
+ , textOverlayFontStyle
+ , "-stretch"
+ , textOverlayFontStretch
+ , "-weight"
+ , show textOverlayFontWeight
+ , "-pointsize"
+ , show textOverlayFontSize
+ , "-gravity"
+ , show textOverlayOrigin
+ , "-density"
+ , "96"
+ ]
+ ++ ( if textOverlayOutlineSize <= 0
+ then []
+ else
+ [ "-strokewidth"
+ , show textOverlayOutlineSize
+ , "-stroke"
+ , textOverlayOutlineColor
+ , "-annotate"
+ , rotation textOverlayRotation
+ ++ position textOverlayOrigin textOverlayXTranslation textOverlayYTranslation
+ , textOverlayText
+ ]
+ )
+ ++ ["-stroke"
+ , "none"
+ , "-fill"
+ , textOverlayFillColor
+ , "-annotate"
+ , rotation textOverlayRotation
+ ++ position textOverlayOrigin textOverlayXTranslation textOverlayYTranslation
+ , textOverlayText
+ ]
+ )
+ []
+ textOverlays
+ let params =
+ [ "-quiet"
+ , filePath
+ ]
+ ++ annotations
+ ++ [ "-set"
+ , "colorspace"
+ , "sRGB"
+ ]
+ ++ [filePath]
+ result <- try $ readProcess "convert" params []
+ if isLeft result
+ then return result
+ else return $ Right $ "Annotated " ++ filePath
+ where
+ {-
+ .+ .+ +.
+ + + +
+
+ .+ .+ +.
+ + + +
+
+ + + +
+ . + .+ +.
+ -}
+ position :: TextOverlayOrigin -> Float -> Float -> String
+ position TextOverlayOriginNorthWest textOverlayXTranslation textOverlayYTranslation =
+ toString (x pos textOverlayXTranslation 1.0 0.0)
+ ++ toString (y pos textOverlayYTranslation 1.0 0.0)
+ position TextOverlayOriginNorth textOverlayXTranslation textOverlayYTranslation =
+ toString (x pos textOverlayXTranslation 0.5 0.5)
+ ++ toString (y pos textOverlayYTranslation 1.0 0.0)
+ position TextOverlayOriginNorthEast textOverlayXTranslation textOverlayYTranslation =
+ toString (x neg textOverlayXTranslation 0.0 1.0)
+ ++ toString (y pos textOverlayYTranslation 1.0 0.0)
+ position TextOverlayOriginWest textOverlayXTranslation textOverlayYTranslation =
+ toString (x pos textOverlayXTranslation 1.0 0.0)
+ ++ toString (y pos textOverlayYTranslation 0.5 0.5)
+ position TextOverlayOriginCenter textOverlayXTranslation textOverlayYTranslation =
+ toString (x pos textOverlayXTranslation 0.5 0.5)
+ ++ toString (y pos textOverlayYTranslation 0.5 0.5)
+ position TextOverlayOriginEast textOverlayXTranslation textOverlayYTranslation =
+ toString (x neg textOverlayXTranslation 0.0 1.0)
+ ++ toString (y pos textOverlayYTranslation 0.5 0.5)
+ position TextOverlayOriginSouthWest textOverlayXTranslation textOverlayYTranslation =
+ toString (x pos textOverlayXTranslation 1.0 0.0)
+ ++ toString (y neg textOverlayYTranslation 0.0 1.0)
+ position TextOverlayOriginSouth textOverlayXTranslation textOverlayYTranslation =
+ toString (x pos textOverlayXTranslation 0.5 0.5)
+ ++ toString (y neg textOverlayYTranslation 0.0 1.0)
+ position TextOverlayOriginSouthEast textOverlayXTranslation textOverlayYTranslation =
+ toString (x neg textOverlayXTranslation 0.0 1.0)
+ ++ toString (y neg textOverlayYTranslation 0.0 1.0)
+ x :: Float -> Float -> Float -> Float -> Float
+ x f t lc rc = f * (originX t - (gifWidthLeftCrop * lc) + (gifWidthRightCrop * rc))
+ y :: Float -> Float -> Float -> Float -> Float
+ y f t tc bc = f * (originY t - (gifHeightTopCrop * tc) + (gifHeightBottomCrop * bc))
+ originX :: Float -> Float
+ originX = (*) gifWidthNoCrop
+ originY :: Float -> Float
+ originY = (*) gifHeightNoCrop
+ gifWidthLeftCrop :: Float
+ gifWidthLeftCrop = gifWidthNoCrop * leftCrop
+ gifWidthRightCrop :: Float
+ gifWidthRightCrop = gifWidthNoCrop * rightCrop
+ gifHeightTopCrop :: Float
+ gifHeightTopCrop = gifHeightNoCrop * topCrop
+ gifHeightBottomCrop :: Float
+ gifHeightBottomCrop = gifHeightNoCrop * bottomCrop
+ neg :: Float
+ neg = -1.0
+ pos :: Float
+ pos = 1.0
+ toString :: Float -> String
+ toString f
+ | f >= 0.0 = "+" ++ show (abs (round f :: Int))
+ | otherwise = "-" ++ show (abs (round f :: Int))
+ rotation :: Int -> String
+ rotation d = show d' ++ "x" ++ show d'
+ where
+ d' :: Int
+ d' = mod d 360
+
mergeFramesIntoGif :: GifParams -> String -> Float -> IO (Either IOError String)
mergeFramesIntoGif
GifParams
{ outputFile
- , saveAsVideo
- , qualityPercent
- , fontChoice
- , topText
- , bottomText
+ , quality
}
tempDir
frameRate
= do
- maybeWidthHeight <-
- maybeGetFirstFrameFilePath tempDir >>=
- maybeGetFirstFrameWidthHeight
- let frameRate' = maybeFrameRateOrDefaultFrameRate (Just frameRate)
- let delay = show $ 100.0 / frameRate'
- let outputFile' =
- if saveAsVideo
- then tempDir ++ "/finished-result.gif"
- else gifOutputFile outputFile
+ let outputFile' = gifOutputFile outputFile
+ let (delay, colors, fuzz) = qualityAndFrameRateToGifSettings quality frameRate
let params =
- [ "-quiet"
- , "-delay"
- , delay
- , tempDir ++ "/*." ++ frameFileExtension
- ]
- ++ annotate fontChoice maybeWidthHeight topText "north"
- ++ annotate fontChoice maybeWidthHeight bottomText "south"
- ++ [ "+dither"
- , "-colors"
- , show $ numberOfColors qualityPercent
- , "-fuzz"
- , "2%"
- , "-layers"
- , "OptimizeFrame"
- , "-layers"
- , "OptimizeTransparency"
- , "-loop"
- , "0"
- , "+map"
- , outputFile'
- ]
+ [ "-quiet"
+ ]
+ ++ delay
+ ++ [ tempDir ++ "/extracted-frames_*." ++ frameFileExtension
+ , "+dither"
+ ]
+ ++ colors
+ ++ fuzz
+ ++ [ "-layers"
+ , "OptimizeFrame"
+ , "-layers"
+ , "OptimizeTransparency"
+ , "-loop"
+ , "0"
+ , "+map"
+ , "-set"
+ , "colorspace"
+ , "sRGB"
+ , outputFile'
+ ]
putStrLn $ "[INFO] Saving your GIF to: " ++ outputFile'
result <- try $ readProcess "convert" params []
if isLeft result
then return result
else return $ Right outputFile'
-convertGifToVideo :: GifParams -> String -> IO (Either IOError String)
-convertGifToVideo GifParams { outputFile } gifFilePath = do
+mergeFramesIntoVideo :: GifParams -> String -> Float -> IO (Either IOError String)
+mergeFramesIntoVideo GifParams { outputFile, quality } tempDir frameRate = do
let outputFile' = videoOutputFile outputFile
let params =
[ "-nostats"
, "-loglevel"
, "error"
, "-y"
+ , "-framerate"
+ , show frameRate
+ , "-start_number"
+ , "0"
, "-i"
- , gifFilePath
+ , tempDir ++ "/extracted-frames_%010d." ++ frameFileExtension
, "-c:v"
, "libvpx-vp9"
+ , "-crf"
+ , show $ targetQuality quality
+ , "-b:v"
+ , "0"
, "-pix_fmt"
- , "yuva420p"
+ , "yuv420p"
, "-vf"
, "scale=trunc(iw/2)*2:trunc(ih/2)*2"
, "-an"
@@ -602,169 +912,248 @@ convertGifToVideo GifParams { outputFile } gifFilePath = do
if isLeft result
then return result
else return (Right outputFile')
+ where
+ targetQuality :: Quality -> Int
+ targetQuality QualityHigh = 15
+ targetQuality QualityMedium = 34
+ targetQuality QualityLow = 37
-qualityPercentClamp :: Float -> Float
-qualityPercentClamp qualityPercent
- | qualityPercent > 100.0 = 100.0
- | qualityPercent < 0.0 = 1.0
- | otherwise = qualityPercent
-
-numberOfColors :: Float -> Int
-numberOfColors qualityPercent
- | qualityPercentClamp qualityPercent <= 1.0 = 2
- | qualityPercentClamp qualityPercent >= 100.0 = floor maxColors
- | otherwise = truncate $ (qualityPercent / 100.0) * maxColors
+getVideoAverageFrameRateInSeconds :: GifParams -> IO (Maybe Float)
+getVideoAverageFrameRateInSeconds GifParams { inputFile } = tryFfprobe params >>= result
where
- maxColors :: Float
- maxColors = 256.0
-
-annotate :: String -> Maybe (Int, Int) -> String -> String -> [String]
-annotate _ _ "" _ = []
-annotate fontChoiceArg maybeWidthHeight text gravity =
- [ "-gravity"
- , gravity
- ]
- ++ fontSetting fontChoiceArg
- ++ [ "-stroke"
- , "#000C"
- , "-strokewidth"
- , "10"
- , "-density"
- , "96"
- , "-pointsize"
- , pointsize
- , "-annotate"
- , "+0+10"
- , text
- , "-stroke"
- , "none"
- , "-fill"
- , "white"
- , "-density"
- , "96"
- , "-pointsize"
- , pointsize
- , "-annotate"
- , "+0+10"
- , text
+ result :: Either IOError String -> IO (Maybe Float)
+ result (Left _) = return Nothing
+ result (Right avgFrameRateString) = return $ processString avgFrameRateString
+ where
+ processString :: String -> Maybe Float
+ processString =
+ divideMaybeFloats . textsToMaybeFloats . filterNullTexts . splitText . cleanString
+ cleanString :: String -> Text
+ cleanString = Data.Text.strip . Data.Text.pack
+ splitText :: Text -> [Text]
+ splitText = Data.Text.split (== '/')
+ filterNullTexts :: [Text] -> [Text]
+ filterNullTexts = Data.List.filter (not . Data.Text.null)
+ textsToMaybeFloats :: [Text] -> [Maybe Float]
+ textsToMaybeFloats =
+ Data.List.filter isJust
+ . Data.List.map (\ s -> readMaybe (Data.Text.unpack s) :: Maybe Float)
+ divideMaybeFloats :: [Maybe Float] -> Maybe Float
+ divideMaybeFloats (Just n:Just d:_) =
+ if d <= 0 || n <= 0 then Nothing else Just $ n / d
+ divideMaybeFloats _ = Nothing
+ params :: [String]
+ params =
+ [ "-v"
+ , "error"
+ , "-select_streams"
+ , "v:0"
+ , "-show_entries"
+ , "stream=avg_frame_rate"
+ , "-of"
+ , "default=noprint_wrappers=1:nokey=1"
+ , inputFile
]
+
+tryFfprobe :: [String] -> IO (Either IOError String)
+tryFfprobe = tryProcess "ffprobe"
+
+tryProcess :: String -> [String] -> IO (Either IOError String)
+tryProcess process params = try $ readProcess process params []
+
+qualityAndFrameRateToGifSettings :: Quality -> Float -> ([String], [String], [String])
+qualityAndFrameRateToGifSettings quality@QualityHigh frameRate =
+ ( ["-delay"
+ , qualityAndFrameRateToDelay quality frameRate
+ ]
+ , [ "-colors"
+ , show $ toInt $ 256.0 * 1.0
+ ]
+ , [ "-fuzz"
+ , "1%"
+ ]
+ )
+qualityAndFrameRateToGifSettings quality@QualityMedium frameRate =
+ ( [ "-delay"
+ , qualityAndFrameRateToDelay quality frameRate
+ ]
+ , [ "-colors"
+ , show $ toInt $ 256.0 * 0.75
+ ]
+ , [ "-fuzz"
+ , "2%"
+ ]
+ )
+qualityAndFrameRateToGifSettings quality@QualityLow frameRate =
+ ( [ "-delay"
+ , qualityAndFrameRateToDelay quality frameRate
+ ]
+ , [ "-colors"
+ , show $ toInt $ 256.0 * 0.5
+ ]
+ , [ "-fuzz"
+ , "3%"
+ ]
+ )
+
+qualityAndFrameRateToDelay :: Quality -> Float -> String
+qualityAndFrameRateToDelay quality frameRate =
+ if delay <= 2
+ then "2"
+ else show delay
where
- pointsize :: String
- pointsize = show $ pointSize maybeWidthHeight text
-
--- @96 PPI: w 71 px x h 96 px
-pointSize :: Maybe (Int, Int) -> String -> Int
-pointSize Nothing _ = 0
-pointSize (Just (width, height)) text
- | width <= 0 || height <= 0 = 0
- | textLength <= 0 = 0
- | otherwise = Prelude.minimum [widthLTHeight, widthGTEHeight]
+ delay :: Int
+ delay = toInt $ 100.0 / qualityAndFrameRateToFrameRate quality frameRate
+
+qualityAndFrameRateToFrameRate :: Quality -> Float -> Float
+qualityAndFrameRateToFrameRate QualityHigh frameRate = safeFrameRate $ 1.00 * frameRate
+qualityAndFrameRateToFrameRate QualityMedium frameRate = safeFrameRate $ 0.75 * frameRate
+qualityAndFrameRateToFrameRate QualityLow frameRate = safeFrameRate $ 0.50 * frameRate
+
+safeFrameRate :: Float -> Float
+safeFrameRate frameRate
+ | frameRate <= defaultFrameRate = defaultFrameRate
+ | frameRate >= 50.0 = 50.0
+ | otherwise = frameRate
+
+defaultFrameRate :: Float
+defaultFrameRate = 15.0
+
+fontFamilyArg :: [Text] -> String -> [String]
+fontFamilyArg fontFamilies fontFamily = ["-family", fontFamily']
where
- textLength :: Int
- textLength = Prelude.length text
- width' :: Double
- width' = fromIntegral width
- height' :: Double
- height' = fromIntegral height
- textLength' :: Double
- textLength' = fromIntegral textLength
- widthLTHeight :: Int
- widthLTHeight = truncate $ ((width' * (5.0 / 7.0)) / textLength') * (96.0 / 71.0)
- widthGTEHeight :: Int
- widthGTEHeight = truncate $ height' * (1.0 / 5.0)
-
-fontSetting :: String -> [String]
-fontSetting "" = []
-fontSetting font = ["-font", font]
-
-bestFontNameMatch :: String -> [Text] -> String
-bestFontNameMatch _ [] = "default"
-bestFontNameMatch _ [""] = "default"
-bestFontNameMatch query fontNames = Data.Text.unpack $ bestMatch $ maximumMatch $ Data.Text.pack query
+ fontFamily' :: String
+ fontFamily' = findFontFamily fontFamilies fontFamily
+
+findFontFamily :: [Text] -> String -> String
+findFontFamily fontFamilies fontFamily =
+ if hasFontFamily fontFamilies fontFamily
+ then fontFamily
+ else Data.Text.unpack $ getSansFontFamily fontFamilies
+
+hasFontFamily :: [Text] -> String -> Bool
+hasFontFamily fontFamilies fontFamily =
+ Prelude.any ((== fontFamily') . Data.Text.toLower) fontFamilies
where
- bestMatch :: (Int, Text) -> Text
- bestMatch (s, f) = if s <= 0 then "default" else f
- maximumMatch :: Text -> (Int, Text)
- maximumMatch query' =
- maximumBy (\ (ls, _) (rs, _) -> if ls >= rs then GT else LT) $
- Prelude.map (\ fontName -> (score query' (Data.Text.toLower fontName), fontName)) fontNames
- score :: Text -> Text -> Int
- score query' fontName = sum $ Prelude.map tokenScore (queryTokens query')
- where
- queryTokens :: Text -> [Text]
- queryTokens = Prelude.map cleanQueryToken . Data.Text.splitOn " "
- where
- cleanQueryToken :: Text -> Text
- cleanQueryToken = Data.Text.replace "," "" . Data.Text.toLower . Data.Text.strip
- tokenScore :: Text -> Int
- tokenScore token
- | Data.Text.length token < 1 = 0
- | Data.Text.isInfixOf token fontName = isInfixOfFontName token
- | otherwise = 0
- where
- isInfixOfFontName :: Text -> Int
- isInfixOfFontName token'
- | token' `elem` ["bold", "medium", "light", "regular", "italic"] = 1
- | isNothing (readMaybe (Data.Text.unpack token') :: Maybe Int) = 3
- | otherwise = 0
-
-getListOfFontNames :: IO [Text]
-getListOfFontNames = do
- (_, stdout, _) <- readProcessWithExitCode "convert" ["-list", "font"] []
- let fontNames =
- Prelude.map (Data.Text.strip . Data.Text.drop 5 . Data.Text.strip) $
- Prelude.filter (Data.Text.isInfixOf "font:" . Data.Text.toLower) $
- Data.Text.splitOn "\n" $
- Data.Text.strip $
- Data.Text.pack stdout
- return fontNames
-
-maybeGetFirstFrameFilePath :: String -> IO (Maybe FilePath)
-maybeGetFirstFrameFilePath tempDir =
- try (makeAbsolute tempDir) >>= tryListDir >>= maybeFirstFilePath
+ fontFamily' :: Data.Text.Text
+ fontFamily' = Data.Text.toLower $ Data.Text.pack fontFamily
+
+getSansFontFamily :: [Text] -> Text
+getSansFontFamily fontFamilies
+ | notNull preferedFontFamily = preferedFontFamily
+ | notNull' sansFontFamilies = Prelude.head sansFontFamilies
+ | otherwise = "Sans"
where
- tryListDir :: Either IOError FilePath -> IO (FilePath, Either IOError [FilePath])
- tryListDir (Left y) = return ("", Left y)
- tryListDir (Right dir) = try (listDirectory dir) >>= \ e -> return (dir, e)
- maybeFirstFilePath :: (FilePath, Either IOError [FilePath]) -> IO (Maybe FilePath)
- maybeFirstFilePath (_, Left _) = return Nothing
- maybeFirstFilePath (_, Right []) = return Nothing
- maybeFirstFilePath (dir, Right (x:_)) = return (Just (normalise $ joinPath [dir, x]))
-
-maybeGetFirstFrameWidthHeight :: Maybe FilePath -> IO (Maybe (Int, Int))
-maybeGetFirstFrameWidthHeight Nothing = return Nothing
-maybeGetFirstFrameWidthHeight (Just dir) =
- readProcessWithExitCode "identify" [dir] [] >>=
- \ (_, stdout, _) ->
- maybeConvertWidthHeightString $
- findWidthHeightString $
- splitOn " " $
- Data.Text.pack stdout
+ preferedFontFamily :: Text
+ preferedFontFamily =
+ Prelude.foldl
+ (\ xs x ->
+ if notNull xs
+ then xs
+ else
+ if contains "dejavu" x ||
+ contains "ubuntu" x ||
+ contains "droid" x ||
+ contains "open" x ||
+ contains "helvetica" x ||
+ contains "arial" x
+ then x
+ else ""
+ )
+ ""
+ sansFontFamilies
+ notNull' :: [Text] -> Bool
+ notNull' = Prelude.not . Prelude.null
+ notNull :: Text -> Bool
+ notNull = Prelude.not . Data.Text.null
+ contains :: Text -> Text -> Bool
+ contains h n = Data.Text.isInfixOf h $ Data.Text.toLower n
+ sansFontFamilies :: [Text]
+ sansFontFamilies =
+ Prelude.filter
+ (Data.Text.isInfixOf "sans" . Data.Text.toLower)
+ fontFamilies
+
+getFontFamilies :: IO [Text]
+getFontFamilies = do
+ (_, stdout, _) <- readProcessWithExitCode "convert" ["-list", "font"] []
+ let fontFamilies =
+ Prelude.map
+ (Data.Text.strip . Data.Text.drop 7 . Data.Text.strip) $
+ Prelude.filter (Data.Text.isInfixOf "family:" . Data.Text.toLower) $
+ Data.Text.splitOn "\n" $
+ Data.Text.strip $
+ Data.Text.pack stdout
+ return fontFamilies
+
+getFrameNumbers :: [String] -> Maybe [Int]
+getFrameNumbers filePaths =
+ if Prelude.length frameNumbers == Prelude.length filePaths
+ then Just frameNumbers
+ else Nothing
where
- findWidthHeightString :: [Text] -> Text
- findWidthHeightString (_:_:c:_:_:_:_:_:_:_) = c
- findWidthHeightString _ = ""
- maybeConvertWidthHeightString :: Text -> IO (Maybe (Int, Int))
- maybeConvertWidthHeightString "" = return Nothing
- maybeConvertWidthHeightString s =
- if Prelude.length splitOnX == 2
- then return (Just (pluckWidth splitOnX, pluckHeight splitOnX))
- else return Nothing
+ maybeFrameNumbers :: [Maybe Int]
+ maybeFrameNumbers = Prelude.map (getFrameNumber . System.FilePath.takeFileName) filePaths
+ frameNumbers :: [Int]
+ frameNumbers = Prelude.foldl folder [] maybeFrameNumbers
where
- splitOnX :: [Text]
- splitOnX = splitOn "x" $ Data.Text.toLower s
- pluckWidth :: [Text] -> Int
- pluckWidth (x:_:_) = read (Data.Text.unpack x) :: Int
- pluckWidth _ = 0
- pluckHeight :: [Text] -> Int
- pluckHeight (_:y:_) = read (Data.Text.unpack y) :: Int
- pluckHeight _ = 0
-
-fontChoiceOrDefault :: GifParams -> String
-fontChoiceOrDefault GifParams { fontChoice = fontName } =
- if Data.List.null cleanedFontName
- then defaultFontChoice
- else cleanedFontName
+ folder :: [Int] -> Maybe Int -> [Int]
+ folder xs (Just int) = xs ++ [int]
+ folder xs Nothing = xs
+
+getFrameNumber :: String -> Maybe Int
+getFrameNumber s =
+ readMaybe (parsedResult s) :: Maybe Int
+ where
+ parsedResult :: String -> String
+ parsedResult s' =
+ case readP_to_S parseFileName s' of
+ [(x, _)] -> x
+ _ -> ""
+ parseFileName :: ReadP String
+ parseFileName = do
+ _ <- string "extracted-frames_"
+ digits <- parseNumber
+ _ <- char '.'
+ return digits
+
+getRgb :: String -> Maybe (Int, Int, Int)
+getRgb s =
+ case parsedResult s of
+ (r, g, b) ->
+ case (readMaybe' r, readMaybe' g, readMaybe' b) of
+ (Just r', Just g', Just b') -> Just (r', g', b')
+ _ -> Nothing
+ where
+ readMaybe' :: String -> Maybe Int
+ readMaybe' = readMaybe
+ parsedResult :: String -> (String, String, String)
+ parsedResult s' =
+ case readP_to_S parseRgb s' of
+ [((r,g,b), _)] -> (r, g, b)
+ _ -> ("", "", "")
+ parseRgb :: ReadP (String, String, String)
+ parseRgb = do
+ _ <- string "rgb("
+ r <- parseNumber
+ _ <- char ','
+ g <- parseNumber
+ _ <- char ','
+ b <- parseNumber
+ _ <- char ')'
+ return (r, g, b)
+
+parseNumber :: ReadP String
+parseNumber = many (satisfy isNumber)
where
- cleanedFontName :: String
- cleanedFontName = (Data.Text.unpack . Data.Text.strip . Data.Text.pack) fontName
+ isNumber :: Char -> Bool
+ isNumber = flip elem numbers
+ numbers :: String
+ numbers = "0123456789"
+
+toInt :: Float -> Int
+toInt = round
+
+stripAndLowerString :: String -> String
+stripAndLowerString =
+ Data.Text.unpack . Data.Text.toLower . Data.Text.strip . Data.Text.pack
diff --git a/stack.yaml b/stack.yaml
index 104d6c8..8f4c725 100644
--- a/stack.yaml
+++ b/stack.yaml
@@ -5,6 +5,8 @@ packages:
explicit-setup-deps:
! '*': true
extra-deps:
+ - gi-pango-1.0.15
+ - gi-gdkpixbuf-2.0.15
- gi-gst-1.0.15
- gi-gtk-3.0.20
- gi-cairo-1.0.15