packages feed

Gifcurry 2.3.0.0 → 3.0.0.0

raw patch · 16 files changed

+4253/−1461 lines, 16 filesdep +bytestringdep +cairodep +gi-cairodep ~basedep ~haskell-gi-base

Dependencies added: bytestring, cairo, gi-cairo, gi-gst, gi-gstvideo, haskell-gi, transformers

Dependency ranges changed: base, haskell-gi-base

Files

CHANGELOG.md view
@@ -2,6 +2,39 @@  ## Changelog +-------------------------------------------------------------------------------++### 3.0.0.0++#### Added++- Added a video preview using GStreamer+- Added start time and end time indicators to UI+- Added a save as video option to both the GUI and CLI+- Added a crop mode to the UI+- Added crop parameters to the CLI+- Added the following to the library interface+    - `getOutputFileWithExtension`+    - `getVideoWidthAndHeight`+    - `defaultFontChoice`+- Added snap distribution++#### Changed++- Reconfigured the UI to show the preview off to the right along with the start and duration controls+- Changed the image first and last frame preview to be the fallback if GStreamer errors+- Changed text entries to spin buttons for better error checking+    - The spin buttons provide better feedback about the min and max settings+- Moved the GUI preview code into a separate module+- Locked the GHC version to avoid build issues concerning haskell-gi and GHC 8.2.X+- Changed the logo and icon++#### Removed++-++-------------------------------------------------------------------------------+ ### 2.3.0.0  #### Added
Gifcurry.cabal view
@@ -1,7 +1,7 @@ name:                 Gifcurry-version:              2.3.0.0+version:              3.0.0.0 synopsis:             GIF creation utility.-description:          Create animated GIFs, optionally overlaid with text, from video files.+description:          Your open source video to GIF maker. homepage:             https://github.com/lettier/gifcurry license:              BSD3 license-file:         LICENSE@@ -16,16 +16,17 @@                       , LICENSE                       , CHANGELOG.md                       , ./lib/GtkMainSyncAsync.hs+                      , ./lib/GiCairoCairoBridge.hs                       , ./lib/LICENSE                       , ./src/dev/Paths_Gifcurry.hs                       , ./src/data/gifcurry-logo.svg-                      , ./src/data/gifcurry-icon.png+                      , ./src/data/gifcurry-icon.svg                       , ./makefile data-files:             data/gui.glade                       , data/gifcurry-logo.svg-                      , data/gifcurry-icon.png+                      , data/gifcurry-icon.svg data-dir:             ./src/-cabal-version:        >=1.10+cabal-version:        >= 1.10  source-repository head   type:     git@@ -46,19 +47,33 @@  executable            gifcurry_gui   main-is:            Main.hs-  build-depends:        base >= 4.7 && < 5-                      , haskell-gi-base == 0.20.*-                      , gi-glib == 2.0.*+  build-depends:        base == 4.9.*+                      , haskell-gi == 0.21.*+                      , haskell-gi-base == 0.21.*                       , gi-gobject == 2.0.*-                      , gi-gtk == 3.0.*+                      , gi-glib == 2.0.*                       , gi-gdk == 3.0.*+                      , gi-gtk == 3.0.*+                      , gi-cairo == 1.0.*+                      , gi-gst == 1.0.*+                      , gi-gstvideo == 1.0.*+                      , cairo == 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.*-  other-modules:      Paths_Gifcurry, GtkMainSyncAsync, Gifcurry-  ghc-options:        -threaded -with-rtsopts=-N -Wall -freverse-errors+                      , transformers == 0.5.*+  other-modules:        Paths_Gifcurry+                      , GuiRecords+                      , Gifcurry+                      , GuiPreview+                      , GuiMisc++                      , GtkMainSyncAsync+                      , GiCairoCairoBridge+  ghc-options:        -threaded -rtsopts -with-rtsopts=-N -Wall -freverse-errors   hs-source-dirs:       ./lib/                       , ./src/                       , ./src/gui/
README.md view
@@ -1,254 +1,359 @@-![Gifcurry](http://i.imgur.com/jhU7puN.png)+![Gifcurry](https://i.imgur.com/t6zJGR9.png) -# Gifcurry+# Tell me about Gifcurry. -Create animated GIFs, optionally overlaid with text, from video files.+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. -## Screenshots+For the command line averse, there is a GUI.+Die-hard terminal aficionado? Gifcurry has you covered with its CLI.+And for the Haskell programmers out there, there is also a library API. -![GUI](https://i.imgur.com/U8xnK82.gif)+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. -## Sample GIF+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. -![Caminandes 3: Llamigos (2016) - Blender Foundation](https://i.imgur.com/FJW2gBc.gif)  -Credit: [Caminandes 3: Llamigos (2016) - Blender Foundation](http://www.caminandes.com/)+Gifcurry caters to the power user with its crop tool.+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. -## CLI Usage+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. +I know what your thinkin', "Gifcurry is just FFMpeg and ImageMagick," but you'd be wrong.+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.  +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.  ++Gifcurry comes in handy for all sorts of scenarios.++## What does the GUI look like?++![GUI](https://i.imgur.com/oYQAyQY.png)+![GUI](https://i.imgur.com/er3AQjP.png)+![GUI](https://i.imgur.com/mW1Rsd1.png)+![GUI](https://i.imgur.com/IQqkxfB.png)+![GUI](https://i.imgur.com/NMadQxQ.png)++## 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 gifcurry_cli [OPTIONS] -Common flags:-  -i --inputfile=FILE      The input video file path and name.-  -o --outputfile=ITEM     The output GIF file path and name.-  -s --starttime=NUM       The start time (in seconds) for the first frame.-  -d --durationtime=NUM    How long the GIF lasts (in seconds) from the start-                           time.-  -w --widthsize=INT       How wide the GIF needs to be. Height will scale to-                           match.-  -q --qualitypercent=NUM  Ranges from 0.0 to 100.0.-  -f --fontchoice=ITEM     Choose your desired font for the top and bottom-                           text.-  -t --toptext=ITEM        The text you wish to add to the top of the GIF.-  -b --bottomtext=ITEM     The text you wish to add to the bottom of the GIF.-  -? --help                Display help message-  -V --version             Print version information-     --numeric-version     Print just the version number+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.+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.+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 ``` -## CLI Example+## Got a CLI example? -```Bash-~/gifcurry ❯❯❯ ./gifcurry_cli \-  -i ./03_caminandes_llamigos_1080p.mp4 \-  -o ./out.gif \-  -s 42 \-  -d 4 \-  -w 600 \-  -q 100 \-  -f 'Roboto Condensed Bold Italic' \-  -t 'Download' \-  -b 'Gifcurry'+```text+gifcurry_cli \+-i ~/Videos/video.mp4 -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' - _____ _  __                           -|  __ (_)/ _|                          -| |  \/_| |_ ___ _   _ _ __ _ __ _   _ -| | __| |  _/ __| | | | '__| '__| | | |-| |_\ \ | || (__| |_| | |  | |  | |_| |- \____/_|_| \___|\__,_|_|  |_|   \__, |-                                  __/ |-                                 |___/ +       ╓╦NÑ╫╫╫╫╫ÑNw,                                                           +    , `Ñ╫╫╫╫Ñ `'╩╫╫╫Ñw      ,╓ææµ  ║▓▓⌐ ,▄▄▓▄                                  +   ]╫Ñ  Ñ╫╫╫ ,╫N ╙╫╫╫╫Ñ   ╓▓▓▀╙╙▀╨ .╓, ,▓▓▌,  ,╓╥╓, ,,  ,,, ,, ,╓ ,, ,╓ ,,  ., +  ]╫╫╫Ñ '╫╫⌐ Ñ╫⌐  Ñ╫╫╫╫Ñ -▓▓M ╥╥╥╥ ▐▓▓ ▀▓▓▀▀ ▄▓▀╙▀▀ ▓▓Γ ╫▓▌ ╫▓▓▌▀ ▓▓▓▀▀╙▓▓  ▓▓▀+  ╩╫╫╫╫H ╠Ñ 1╫H ╫ '╫╫╫╫╣  ▓▓H ╙▐▓▓ ▐▓▓  ▓▓Γ ▐▓▓⌐    ▓▓Γ ╫▓▌ ╫▓▌   ▓▓▌   ║▓▌╢▓▌ +  ]╫╫╫╫╫ ' j╫Å j╫H ╠╫╫╫╫  ╙▓▓▄▄▄▓▓ ▐▓▓  ▓▓Γ  ▀▓▌▄▄▄ ▓▓▌▄▓▓▌ ╫▓▌   ▓▓▌    ▓▓▓▓  +   Ñ╫╫╫╫N jÑ╬ .╫╫╫⌐ Ñ╫╬┘    └╙╙╙└  `└└  └└`    ╙╙╙`  ╙╙└`└` `└└   └└`    ,▓▓▀  +    ╨╫╫╫╫N '┘ ╬╫╫╫╫  ╩`                                                 ▀▀▀└   +     `╙Ñ╫╫╫N╦╦╫╫╫╫╫╩                                                           +         `'╙╙╙╙'``                                                              -Gifcurry 2.3.0.0++Gifcurry 3.0.0.0 (C) 2016 David Lettier lettier.com -----------------------------------------+[INFO] Here are your settings. -Input file: ./03_caminandes_llamigos_1080p.mp4-Output file: ./out.gif-Start second: 42.000-Duration: 4.000 seconds-GIF width: 600px-Quality: 100.0%-Font Choice: Roboto Condensed Bold Italic-Top text: Download-Bottom text: Gifcurry+FILE IO: -Writing temporary frames to... ./frames13465-Font matched: Roboto-Condensed-Bold-Italic-Writing your GIF to... ./out.gif-Done.-```+Input File: /home/Videos/video.mp4+Output File: /home/tmp/test.mp4+Save As Video: Yes -## Dependencies+TIME: -### Run+Start Second: 149.110+Duration Time: 1.000 seconds -* [GTK+](http://www.gtk.org/download/index.php)-* [FFmpeg](https://www.ffmpeg.org/download.html)-* [ImageMagick](http://www.imagemagick.org/script/download.php)+OUTPUT FILE SIZE: -### Build+Width Size: 500px+Quality Percent: 100.0% -* [Haskell](https://www.haskell.org/platform/)+TEXT: -## Install & Run+Font Choice: fira sans+Top Text: Top Text+Bottom Text: Bottom Text -### Linux+CROP: -#### [AppImage (GUI Only)](https://appimage.github.io/gifcurry/)+Left Crop: 25.000+Right crop: 25.000+Top Crop: 25.000+Bottom Crop: 25.000 -```bash-# Install FFmpeg (https://www.ffmpeg.org/download.html)-# Install ImageMagick (https://www.imagemagick.org/script/download.php)-# Install GNU Wget (https://www.gnu.org/software/wget/) or download the AppImage with your browser-# Find the latest release at https://github.com/lettier/gifcurry/releases-# * equals the version number-# Download gifcurry-*-x86_64.AppImage-# Right click on the AppImage and allow the file to be executed as a program-wget https://github.com/lettier/gifcurry/releases/download/*/gifcurry-*-x86_64.AppImage-chmod a+x gifcurry-*-x86_64.AppImage-./gifcurry-*-x86_64.AppImage-# If you would like to install the AppImage, you can run the following-cd-wget "https://raw.githubusercontent.com/lettier/gifcurry/master/packaging/linux/gifcurry-gui-install.sh" -O "gifcurry-gui-install.sh"-chmod +x "gifcurry-gui-install.sh"-./gifcurry-gui-install.sh+[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+[INFO] Saving your video to: /home/tmp/test.mp4+[INFO] All done. ``` -#### Ubuntu/Mint+## How do I get a copy of Gifcurry? -```bash-# Install FFmpeg (https://www.ffmpeg.org/download.html)-# Install ImageMagick (https://www.imagemagick.org/script/download.php)-sudo add-apt-repository ppa:jonathonf/ffmpeg-3-sudo apt-get update-sudo apt-get install ffmpeg imagemagick-# Install GNU Wget (https://www.gnu.org/software/wget/)-# Find the latest release at https://github.com/lettier/gifcurry/releases-# * equals the version number-wget https://github.com/lettier/gifcurry/releases/download/*/gifcurry-linux-*.tar.gz-tar -xvfz gifcurry-linux*.tar.gz-cd gifcurry-linux-*/bin-./gifcurry_cli -?-./gifcurry_gui-```+Gifcurry works on Linux, Mac, and probably Windows (left me know).+Make sure you have FFmpeg, GStreamer, ImageMagick, and GTK+ installed on your machine.+To find the latest version of Gifcurry, head over to the+[releases page](https://github.com/lettier/gifcurry/releases). -#### Arch/Manjaro/Antergos+### 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.0/gifcurry-3.0.0.0-x86_64.AppImage).+After you download the+[AppImage](https://github.com/lettier/gifcurry/releases/download/3.0.0.0/gifcurry-3.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.0/gifcurry-3.0.0.0-x86_64.AppImage)+using the handy+[AppImage install script](https://raw.githubusercontent.com/lettier/gifcurry/master/distribution/linux/app-image/gifcurry-app-image-install.sh)+(right click and save link as).+Download the script, right click on it, select permissions, check the box near execute, and double click on it.+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.0/gifcurry-linux-3.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+too so now you have both.++#### I use Arch/Manjaro/Antergos/pacman.++If you'd rather install Gifcurry via pacman then copy the following into your terminal.+ ```bash cd-# Install Git+# Install git. sudo pacman -S git-# Install Gifcurry from AUR+# Install Gifcurry. mkdir -p build_gifcurry cd build_gifcurry git clone https://aur.archlinux.org/gifcurry.git cd gifcurry makepkg -sic+# Run Gifcurry CLI and GUI. cd rm -rf build_gifcurry gifcurry_cli -? gifcurry_gui ``` -### Mac+#### I use Ubuntu/Mint/Debian/Deepin/snap. -#### Binaries+Gifcurry is available as a snap from [Snapcraft](https://snapcraft.io/).+If you don't already have `snap`, go ahead and install it using the command `sudo apt install snapd`. +You can install the+[Gifcurry snap](https://snapcraft.io/gifcurry)+right from your browser or via the command line.+For the command line route, paste the following into your terminal.+ ```bash-# Install Homebrew-/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"-brew update-brew install ffmpeg-brew install imagemagick-brew install ghostscript-brew install wget-brew install gnome-icon-theme-# Find the latest release at https://github.com/lettier/gifcurry/releases-# * equals the version number-wget https://github.com/lettier/gifcurry/releases/download/*/gifcurry-macosx-*.tar.gz-tar xvfz gifcurry-macosx-*.tar.gz-cd gifcurry-macosx-*/bin-./gifcurry_cli -?-./gifcurry_gui+snap install gifcurry+sudo snap connect gifcurry:mount-observe+sudo snap connect gifcurry:removable-media+sudo snap connect gifcurry:raw-usb+gifcurry ``` -#### Compile+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.0/gifcurry-linux-3.0.0.0.tar.gz)+for Linux. +### I use Mac.++If you use Mac then you'll need to find the terminal.+Open Spotlight, type `Terminal`, and press enter.+You should see a window open that has black text+on a white background.++With the terminal open, copy the following into the terminal to install Homebrew.+ ```bash-# Install Homebrew /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" brew update-brew install xcode-brew install git-git clone git@github.com:lettier/gifcurry.git+```++After installing Homebrew, you'll need to install the right dependencies.++```bash+xcode-select --install+brew install \+  wget \+  git \+  libffi \+  libsvg \+  librsvg \+  libav \+  libogg \+  libvorbis \+  pkg-config \+  gobject-introspection \+  cairo \+  gdk-pixbuf \+  gsettings-desktop-schemas \+  gtk+3 \+  gtk-mac-integration \+  gnome-icon-theme \+  openh264 \+  theora \+  ffmpeg \+  imagemagick \+  ghostscript \+  gstreamer \+  gst-libav \+  gst-plugins-base \+  gst-plugins-good+brew install --with-gtk+3 gst-plugins-bad+wget -qO- https://get.haskellstack.org/ | sh -s - -f+```++The next step is to download the Gifcurry source with a program called `git`.+After downloading the source, change directory (`cd`) into the Gifcurry folder.++```bash+git clone https://github.com/lettier/gifcurry.git cd gifcurry/-brew cask install haskell-platform-brew install pkg-config gobject-introspection cairo gdk-pixbuf gsettings-desktop-schemas \-  gtk+3 gtk-mac-integration ffmpeg imagemagick ghostscript gnome-icon-theme-stack setup-stack install hsc2hs-stack install-export PATH=$PATH:"$HOME/.local/bin/"-gifcurry_cli -?-gifcurry_gui ``` -### [Hackage](https://hackage.haskell.org/package/Gifcurry)+Now that you have the source, copy this into the terminal.+It's scary lookin' but it is telling the program that builds+Gifcurry where the `libffi` package configuration is. -#### Stack+```bash+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+``` +We're almost there.+With the `stack` program, install the Haskell specific dependencies and build Gifcurry.+ ```bash-# Install GNU Wget (https://www.gnu.org/software/wget/)-# Install FFmpeg (https://www.ffmpeg.org/download.html)-# Install ImageMagick (https://www.imagemagick.org/script/download.php)-# Install Haskell (https://www.haskell.org/platform/)-# Find the latest version number at https://hackage.haskell.org/package/Gifcurry-# * equals the version number-wget https://hackage.haskell.org/package/Gifcurry-*/Gifcurry-*.tar.gz-tar -xvzf Gifcurry-*.tar.gz-cd Gifcurry-* stack setup+stack install alex happy+stack install gtk2hs-buildtools+stack install hsc2hs stack install-stack exec -- gifcurry_cli -?-stack exec -- gifcurry_gui ``` -#### Cabal+`stack` places the two Gifcurry programs into a special folder.+Copy the following into the terminal to create two shortcuts on your desktop.  ```bash-# Install FFmpeg (https://www.ffmpeg.org/download.html)-# Install ImageMagick (https://www.imagemagick.org/script/download.php)-# Install Haskell (https://www.haskell.org/platform/)-cabal update-cabal install alex happy -j-cabal install gifcurry -j-cd ~/.cabal/bin-./gifcurry_cli -?-./gifcurry_gui+ln -s $HOME/.local/bin/gifcurry_cli $HOME/Desktop/gifcurry_cli+ln -s $HOME/.local/bin/gifcurry_gui $HOME/Desktop/gifcurry_gui ``` -### [Github](https://github.com/lettier/gifcurry)+You can now click on `gifcurry_gui` from your desktop or run `gifcurry_cli` from the terminal. +### I'm a Haskell developer.++If you develop Haskell programs then the easiest way to build Gifcurry is with+[Haskell Stack](https://docs.haskellstack.org/en/stable/README/).+Copy the following into your terminal.+ ```bash-# Install GNU Make (https://www.gnu.org/software/make/)-# Install FFmpeg (https://www.ffmpeg.org/download.html)-# Install ImageMagick (https://www.imagemagick.org/script/download.php)-# Install Haskell (https://www.haskell.org/platform/)-git clone git@github.com:lettier/gifcurry.git-cd gifcurry/-make-make run_cli CLI_ARGS='-?'-make run_gui+git clone https://aur.archlinux.org/gifcurry.git+cd gifcurry+stack setup+stack install alex happy+stack install gtk2hs-buildtools+stack install hsc2hs+stack install+$HOME/.local/bin/gifcurry_cli -?+$HOME/.local/bin/gifcurry_gui ``` -## License+## What dependencies does Gifcurry use? +### To run Gifcurry.++* [GTK+](http://www.gtk.org/download/index.php)+* [FFmpeg](https://www.ffmpeg.org/download.html)+* [GStreamer](https://gstreamer.freedesktop.org/download/)+* [ImageMagick](http://www.imagemagick.org/script/download.php)++### To build Gifcurry.++* [GObject Introspection](https://wiki.gnome.org/action/show/Projects/GObjectIntrospection)+* [Haskell](https://docs.haskellstack.org/en/stable/README/)++## What is the license?+ For license information, see [LICENSE](LICENSE). -## Copyright+## Who wrote Gifcurry?  _(C) 2016 David Lettier_   [lettier.com](http://www.lettier.com/)
+ lib/GiCairoCairoBridge.hs view
@@ -0,0 +1,31 @@+-- original author:+--    Mirco "MacSlow" Mueller <macslow@bangang.de>+--+-- created:+--    10.1.2006 (or so)+--+-- http://www.gnu.org/licenses/licenses.html#GPL+--+-- ported to Haskell (gtk2hs) by:+--    Duncan Coutts <duncan.coutts@worc.ox.ac.uk>+--+-- ported to haskell-gi from the gtk2hs port by:+--    Iñaki García Etxebarria <garetxe@gmail.com>+--+-- Modified by David Lettier++module GiCairoCairoBridge (renderWithContext) where++import Control.Monad.Trans.Reader (runReaderT)+import qualified GI.Cairo+import Graphics.Rendering.Cairo.Internal (Render(runRender))+import Graphics.Rendering.Cairo.Types (Cairo(Cairo))+import Foreign.Ptr (castPtr)++-- | This function bridges gi-cairo with the hand-written cairo+-- package. It takes a `GI.Cairo.Context` (as it appears in gi-cairo),+-- and a `Render` action (as in the cairo lib), and renders the+-- `Render` action into the given context.+renderWithContext :: GI.Cairo.Context -> Render () -> IO ()+renderWithContext ct r = GI.Cairo.withManagedPtr ct $ \p ->+  runReaderT (runRender r) (Cairo (castPtr p))
lib/GtkMainSyncAsync.hs view
@@ -33,10 +33,11 @@  {-# LANGUAGE OverloadedStrings #-} -module GtkMainSyncAsync (-      gtkMainSync-    , gtkMainAsync-  ) where+module GtkMainSyncAsync+  ( gtkMainSync+  , gtkMainAsync+  )+  where  import Control.Monad import Control.Concurrent@@ -59,4 +60,5 @@ gtkMainSyncBuildCallBack callback mVar = callback >>= putMVar mVar >> return False  gtkMainAddCallback :: GI.GLib.SourceFunc -> IO ()-gtkMainAddCallback callback = void $ GI.GLib.idleAdd GI.GLib.PRIORITY_DEFAULT callback+gtkMainAddCallback callback =+  void $ GI.GLib.idleAdd GI.GLib.PRIORITY_DEFAULT callback
makefile view
@@ -9,11 +9,11 @@ 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-VERSION='2.3.0.0'+VERSION='3.0.0.0'  export PATH := $(PATH):$(STACK_PATH_LOCAL_BIN) -all: setup update sandbox_clean clean alex happy install_dependencies configure build install+all: setup update sandbox_clean clean alex happy haskell_gi gtk2hs_buildtools install_dependencies configure build install  setup:   $(STACK) setup && $(STACK) update && $(STACK) install Cabal && $(STACK) install cabal-install@@ -24,6 +24,12 @@ happy: setup   $(STACK) install happy +haskell_gi: setup+  $(STACK) install haskell-gi++gtk2hs_buildtools: setup+  $(STACK) install gtk2hs-buildtools+ sandbox: setup   $(CABAL) sandbox init @@ -65,10 +71,11 @@   --html-location='http://hackage.haskell.org/package/Gifcurry/docs' \   --contents-location='http://hackage.haskell.org/package/Gifcurry' && \   cp -R ./dist/doc/html/Gifcurry/ ./docs/Gifcurry-$(VERSION)-docs && \-  tar --format=ustar -zcvf './docs/Gifcurry-$(VERSION)-docs.tar.gz' './docs/Gifcurry-$(VERSION)-docs'+  cd ./docs && \+  tar --format=ustar -cvf ./Gifcurry-$(VERSION)-docs.tar Gifcurry-$(VERSION)-docs  # Begin Arch Linux Specific-arch_os_build_gifcurry: setup update clean sandbox_clean alex happy arch_os_install_dependencies arch_os_configure arch_os_build+arch_os_build_gifcurry: setup update clean sandbox_clean alex happy haskell_gi gtk2hs_buildtools arch_os_install_dependencies arch_os_configure arch_os_build  arch_os_install_dependencies: sandbox   $(CABAL) --require-sandbox install -j -w $(STACK_GHC_EXE) --force-reinstalls --reinstall --only-dependencies
src/cli/Main.hs view
@@ -4,114 +4,190 @@   lettier.com -} -{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE NamedFieldPuns #-}+{-# LANGUAGE+    DeriveDataTypeable+  , NamedFieldPuns+#-}+{-# OPTIONS_GHC -fno-cse #-} +import Control.Monad import System.Console.CmdArgs  import qualified Gifcurry -data CliArgs = CliArgs {-      inputFile :: String-    , outputFile :: String-    , startTime :: Float-    , durationTime :: Float-    , widthSize :: Int-    , qualityPercent :: Float-    , fontChoice :: String-    , topText :: String-    , bottomText :: String-  } deriving (Data, Typeable, Show, Eq)+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+    }+  deriving (Data, Typeable, Show, Eq) +programName :: String+programName = "gifcurry_cli"+ info :: String-info = "Gifcurry " ++ Gifcurry.versionNumber ++ "\n(C) 2016 David Lettier\nlettier.com"+info =+  unlines+    [ logo+    , "Gifcurry" ++ " " ++ Gifcurry.versionNumber+    , "(C) 2016 David Lettier"+    ,"lettier.com"+    ]  cliArgs :: CliArgs-cliArgs = CliArgs {-      inputFile      = ""        &= typFile &= help "The input video file path and name."-    , outputFile     = ""        &= typFile &= help "The output GIF file path and name."-    , startTime      = 0.0                  &= help "The start time (in seconds) for the first frame."-    , durationTime   = 1.0                  &= help "How long the GIF lasts (in seconds) from the start time."-    , widthSize      = 500                  &= help "How wide the GIF needs to be. Height will scale to match."-    , qualityPercent = 100.0                &= help "Ranges from 0.0 to 100.0."-    , fontChoice     = "default"            &= help "Choose your desired font for the top and bottom text."-    , topText        = ""                   &= help "The text you wish to add to the top of the GIF."-    , bottomText     = ""                   &= help "The text you wish to add to the bottom of the GIF."-  -- VERSION-  } &= summary info-    &= program "gifcurry_cli"+cliArgs =+  CliArgs+    { input_file+        = ""+        &= groupname "FILE IO"+        &= typFile+        &= help "The input video file path and name."+    , output_file+        = ""+        &= groupname "FILE IO"+        &= typFile+        &= help "The output GIF file path and name."+    , save_as_video+        = False+        &= groupname "FILE IO"+        &= name "m"+        &= help "If present, saves the GIF as a video."+    , start_time+        = 0.0+        &= groupname "TIME"+        &= name "s"+        &= help "The start time (in seconds) for the first frame."+    , duration_time+        = 1.0+        &= groupname "TIME"+        &= help "How long the GIF lasts (in seconds) from the start time."+    , width_size+        = 500+        &= groupname "OUTPUT FILE SIZE"+        &= help "How wide the GIF needs to be. Height will scale to match."+    , quality_percent+        = 100.0+        &= 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."+          )+    , 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"+        &= name "L"+        &= help "The amount you wish to crop from the left."+    , right_crop+        = 0.0+        &= groupname "CROP"+        &= name "R"+        &= help "The amount you wish to crop from the right."+    , top_crop+        = 0.0+        &= groupname "CROP"+        &= name "T"+        &= help "The amount you wish to crop from the top."+    , bottom_crop+        = 0.0+        &= groupname "CROP"+        &= name "B"+        &= help "The amount you wish to crop from the bottom."+    }+    &= summary info+    &= program programName+    &= details ["Visit https://github.com/lettier/gifcurry for more information.", ""]  main :: IO () main = do   cliArgs' <- cmdArgs cliArgs   let params = makeGifParams cliArgs'-  printHeader+  putStrLn info   paramsValid <- Gifcurry.gifParamsValid params   if paramsValid-    then do-      _ <- Gifcurry.gif params-      return ()-    else printUsage+    then void $ Gifcurry.gif params+    else+      putStrLn $+        "[INFO] Type \"" ++ programName ++ " -?\" for help."   return ()  makeGifParams :: CliArgs -> Gifcurry.GifParams-makeGifParams CliArgs {-      inputFile-    , outputFile-    , startTime-    , durationTime-    , widthSize-    , qualityPercent-    , fontChoice-    , topText-    , bottomText-  } = Gifcurry.GifParams {-      Gifcurry.inputFile      = inputFile-    , Gifcurry.outputFile     = outputFile-    , Gifcurry.startTime      = startTime-    , Gifcurry.durationTime   = durationTime-    , Gifcurry.widthSize      = widthSize-    , Gifcurry.qualityPercent = qualityPercent-    , Gifcurry.fontChoice     = fontChoice-    , Gifcurry.topText        = topText-    , Gifcurry.bottomText     = bottomText-  }--printUsage :: IO ()-printUsage = putStrLn $ unwords [-        "\n"-      , "Usage:"-      , "\n\t"-      , "$ gifcurry_cli \\"-      , "\n\t"-      , "-i inputFile \\"-      , "\n\t"-      , "-o outputFile \\"-      , "\n\t"-      , "-s startTime \\"-      , "\n\t"-      , "-d durationTime \\"-      , "\n\t"-      , "-w widthSize \\"-      , "\n\t"-      , "-q qualityPercent \\"-      , "\n\t"-      , "-f fontChoice \\"-      , "\n\t"-      , "-t topText \\"-      , "\n\t"-      , "-b bottomText"-  ]+makeGifParams+  CliArgs+    { input_file+    , output_file+    , save_as_video+    , start_time+    , duration_time+    , width_size+    , quality_percent+    , font_choice+    , top_text+    , bottom_text+    , left_crop+    , right_crop+    , top_crop+    , bottom_crop+    }+  =+  Gifcurry.GifParams+    { Gifcurry.inputFile      = input_file+    , Gifcurry.outputFile     = output_file+    , Gifcurry.saveAsVideo    = save_as_video+    , 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.leftCrop       = left_crop+    , Gifcurry.rightCrop      = right_crop+    , Gifcurry.topCrop        = top_crop+    , Gifcurry.bottomCrop     = bottom_crop+    } -printHeader :: IO ()-printHeader = mapM_ putStrLn [-      " _____ _  __                           "-    , "|  __ (_)/ _|                          "-    , "| |  \\/_| |_ ___ _   _ _ __ _ __ _   _ "-    , "| | __| |  _/ __| | | | '__| '__| | | |"-    , "| |_\\ \\ | || (__| |_| | |  | |  | |_| |"-    , " \\____/_|_| \\___|\\__,_|_|  |_|   \\__, |"-    , "                                  __/ |"-    , "                                 |___/ "-    , info-  ]+logo :: String+logo =+  unlines+    [ ""+    , "       ╓╦NÑ╫╫╫╫╫ÑNw,                                                           "+    , "    , `Ñ╫╫╫╫Ñ `'╩╫╫╫Ñw      ,╓ææµ  ║▓▓⌐ ,▄▄▓▄                                  "+    , "   ]╫Ñ  Ñ╫╫╫ ,╫N ╙╫╫╫╫Ñ   ╓▓▓▀╙╙▀╨ .╓, ,▓▓▌,  ,╓╥╓, ,,  ,,, ,, ,╓ ,, ,╓ ,,  ., "+    , "  ]╫╫╫Ñ '╫╫⌐ Ñ╫⌐  Ñ╫╫╫╫Ñ -▓▓M ╥╥╥╥ ▐▓▓ ▀▓▓▀▀ ▄▓▀╙▀▀ ▓▓Γ ╫▓▌ ╫▓▓▌▀ ▓▓▓▀▀╙▓▓  ▓▓▀"+    , "  ╩╫╫╫╫H ╠Ñ 1╫H ╫ '╫╫╫╫╣  ▓▓H ╙▐▓▓ ▐▓▓  ▓▓Γ ▐▓▓⌐    ▓▓Γ ╫▓▌ ╫▓▌   ▓▓▌   ║▓▌╢▓▌ "+    , "  ]╫╫╫╫╫ ' j╫Å j╫H ╠╫╫╫╫  ╙▓▓▄▄▄▓▓ ▐▓▓  ▓▓Γ  ▀▓▌▄▄▄ ▓▓▌▄▓▓▌ ╫▓▌   ▓▓▌    ▓▓▓▓  "+    , "   Ñ╫╫╫╫N jÑ╬ .╫╫╫⌐ Ñ╫╬┘    └╙╙╙└  `└└  └└`    ╙╙╙`  ╙╙└`└` `└└   └└`    ,▓▓▀  "+    , "    ╨╫╫╫╫N '┘ ╬╫╫╫╫  ╩`                                                 ▀▀▀└   "+    , "     `╙Ñ╫╫╫N╦╦╫╫╫╫╫╩                                                           "+    , "         `'╙╙╙╙'``                                                             "+    , ""+    ]
− src/data/gifcurry-icon.png

binary file changed (7692 → absent bytes)

+ src/data/gifcurry-icon.svg view
@@ -0,0 +1,139 @@+<?xml version="1.0" encoding="UTF-8" standalone="no"?>+<svg+   xmlns:dc="http://purl.org/dc/elements/1.1/"+   xmlns:cc="http://creativecommons.org/ns#"+   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:xlink="http://www.w3.org/1999/xlink"+   id="svg8"+   version="1.1"+   viewBox="0 0 57.000001 57"+   height="57mm"+   width="57mm">+  <defs+     id="defs2" />+  <metadata+     id="metadata5">+    <rdf:RDF>+      <cc:Work+         rdf:about="">+        <dc:format>image/svg+xml</dc:format>+        <dc:type+           rdf:resource="http://purl.org/dc/dcmitype/StillImage" />+        <dc:title></dc:title>+      </cc:Work>+    </rdf:RDF>+  </metadata>+  <g+     id="g4638"+     style="display:inline"+     transform="translate(-2.857145,-2.6012127)">+    <path+       id="path6070"+       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" />+    <path+       id="path6068"+       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" />+    <path+       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.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" />+    <path+       id="path4681"+       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" />+    <path+       id="path4648"+       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" />+  </g>+  <g+     id="g4631"+     style="display:inline"+     transform="translate(-3.1131187,-1.2102795)">+    <text+       id="text6160"+       y="38.733902"+       x="61.539043"+       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:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+       xml:space="preserve"><tspan+         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:#2e323c;fill-opacity:1;stroke-width:0.26458332px"+         y="38.733902"+         x="61.539043"+         id="tspan6158">Gifcurr</tspan></text>+    <text+       transform="scale(1,-1)"+       id="text6166"+       y="-20.001196"+       x="172.94928"+       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:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+       xml:space="preserve"><tspan+         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';fill:#2e323c;fill-opacity:1;stroke-width:0.26458332px"+         y="-20.001196"+         x="172.94928"+         id="tspan6164">λ</tspan></text>+    <text+       xml:space="preserve"+       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:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+       x="61.375141"+       y="38.733902"+       id="text6170"><tspan+         id="tspan6168"+         x="61.375141"+         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:#2e323c;fill-opacity:1;stroke-width:0.26458332px">Gifcurr</tspan></text>+    <text+       xml:space="preserve"+       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:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+       x="173.11317"+       y="-20.001196"+       id="text6156"+       transform="scale(1,-1)"><tspan+         id="tspan6154"+         x="173.11317"+         y="-20.001196"+         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';fill:#2e323c;fill-opacity:1;stroke-width:0.26458332px">λ</tspan></text>+    <text+       transform="scale(1,-1)"+       id="text6125"+       y="-20.001196"+       x="172.80458"+       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:#444a58;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+       xml:space="preserve"><tspan+         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';fill:#444a58;fill-opacity:1;stroke-width:0.26458332px"+         y="-20.001196"+         x="172.80458"+         id="tspan6123">λ</tspan></text>+    <text+       xml:space="preserve"+       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:#444a58;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+       x="61.230457"+       y="38.733902"+       id="text6129"><tspan+         id="tspan6127"+         x="61.230457"+         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</tspan></text>+  </g>+  <g+     id="g4644"+     transform="translate(-2.857145,-2.6012127)">+    <path+       id="path4614"+       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" />+    <text+       transform="translate(-0.40960069,0.2054888)"+       id="text4587"+       style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+       xml:space="preserve"><textPath+         id="textPath4616"+         xlink:href="#path4614"><tspan+   id="tspan4585"+   style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;fill:#e8e8e8;fill-opacity:1;stroke-width:0.26458335px"+   dy="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0">© 2018 David Lettier</tspan></textPath></text>+  </g>+</svg>
src/data/gifcurry-logo.svg view
@@ -5,13 +5,15 @@    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"-   viewBox="0 0 384 384"-   height="384"-   width="384"+   width="190.297mm"+   height="54.21814mm"+   viewBox="0 0 190.297 54.21814"    version="1.1"-   id="svg4247">+   id="svg8">+  <defs+     id="defs2" />   <metadata-     id="metadata4253">+     id="metadata5">     <rdf:RDF>       <cc:Work          rdf:about="">@@ -22,122 +24,192 @@       </cc:Work>     </rdf:RDF>   </metadata>-  <defs-     id="defs4251" />   <path-     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"-     d="m 125.90653,283.63875 39.795,55.55914"-     id="path4242" />+     id="path6070"+     d="M 26.895901,1.0049911 A 25.513393,25.513393 0 0 0 11.503043,6.5989714 c 0.133368,0.18103 0.263304,0.36156 0.393258,0.54208 a 25.513393,25.513393 0 0 1 15.683797,-6.1153903 25.513393,25.513393 0 0 0 -0.684197,-0.0207 z M 8.1420083,9.8240914 a 25.513393,25.513393 0 0 0 -6.250264,17.1860296 25.513393,25.513393 0 0 0 25.9973787,25.01191 25.513393,25.513393 0 0 0 0.382921,-0.0207 25.513393,25.513393 0 0 1 -25.3219667,-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.6645596 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" />   <path-     id="path4244"-     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" />+     id="path6068"+     d="m 29.470937,1.0049911 a 25.513393,25.513393 0 0 0 -0.433565,0.0238 25.513393,25.513393 0 0 1 25.219134,24.9726399 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 29.470937,1.0049911 Z M 45.500456,45.710271 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" />   <path-     id="path4246"-     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" />+     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 28.483401,1.0049911 A 25.513393,25.513393 0 0 0 12.420817,7.1343314 c 3.454616,4.7973296 5.736982,9.5883596 8.496639,23.9385896 l 7.284826,-24.5793696 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534349,4.7168696 12.525849,13.2069396 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 28.483401,1.0049911 Z M 9.1414303,10.482451 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.9973787,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.9499027,-22.84408 z m 22.5267897,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="path4679" />   <path-     id="path4248"-     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" />+     id="path4681"+     d="M 28.237847,1.0049911 A 25.513393,25.513393 0 0 0 12.175263,7.1343314 c 3.454616,4.7973296 5.736982,9.5883596 8.496639,23.9385896 l 7.284826,-24.5793696 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534343,4.7168696 12.525843,13.2069396 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 28.237847,1.0049911 Z M 8.8958763,10.482451 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.9973787,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.9499027,-22.84408 z m 22.5267897,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" />   <path-     id="path4250"-     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" />+     id="path4648"+     d="M 27.954234,1.0049911 A 25.513393,25.513393 0 0 0 11.89165,7.1343314 c 3.454616,4.7973296 5.736982,9.5883596 8.496639,23.9385896 l 7.284826,-24.5793696 0.451135,0.11162 0.02223,-0.11162 c 7.861179,1.46742 10.534346,4.7168696 12.525846,13.2069396 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 27.954234,1.0049911 Z M 8.6122633,10.482451 a 25.513393,25.513393 0 0 0 -5.662186,16.52767 25.513393,25.513393 0 0 0 25.9973787,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 21.226845,45.066061 18.553665,41.816611 16.562166,33.326531 13.34987,19.685281 11.390602,14.472901 8.6122633,10.482451 Z m 22.5267897,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" />   <path-     id="path4252"-     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 71.302315,9.6945374 c -6.561666,0 -11.712222,4.5155556 -11.712222,12.8058336 0,8.360834 3.739444,12.841111 11.218334,12.841111 3.069166,0 6.244166,-0.9525 8.784166,-2.575277 V 20.665927 h -9.348611 l 0.564445,4.092222 h 3.175 v 5.538611 c -1.023056,0.564445 -2.116667,0.811389 -3.316112,0.811389 -3.386666,0 -5.08,-2.187222 -5.08,-8.607778 0,-6.067778 2.54,-8.607778 5.961945,-8.607778 1.975555,0 3.316111,0.599722 4.974167,1.869722 l 2.928055,-3.069166 C 77.440649,10.92926 74.865371,9.6945374 71.302315,9.6945374 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5020" />   <path-     id="path4266"-     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 86.102446,6.1314818 c -1.940278,0 -3.280833,1.3758334 -3.280833,3.1750001 0,1.7991671 1.340555,3.1750001 3.280833,3.1750001 1.940278,0 3.316111,-1.375833 3.316111,-3.1750001 0,-1.7991667 -1.375833,-3.1750001 -3.316111,-3.1750001 z m 2.822222,9.8777782 h -5.573889 v 18.7325 h 5.573889 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5022" />   <path-     id="path4268"-     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 101.84516,12.093426 c 0.81139,0 1.86972,0.176389 2.92805,0.705556 l 1.51695,-3.6336112 c -1.37584,-0.7055556 -3.175,-1.1994445 -5.11528,-1.1994445 -4.797779,0 -7.16139,2.7869447 -7.16139,6.3852777 v 1.658056 h -2.822223 v 3.880555 H 94.01349 V 34.74176 h 5.573889 V 19.889815 h 3.810001 l 0.59972,-3.880555 h -4.409721 v -1.446389 c 0,-1.763889 0.635001,-2.469445 2.257781,-2.469445 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5024" />   <path-     id="path4270"-     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 113.58218,15.409538 c -5.60916,0 -9.17222,4.162777 -9.17222,10.124722 0,5.926667 3.52778,9.807222 9.27806,9.807222 2.57527,0 4.58611,-0.846666 6.31472,-2.2225 l -2.43417,-3.457222 c -1.34055,0.846667 -2.25778,1.27 -3.59833,1.27 -2.2225,0 -3.70417,-1.27 -3.70417,-5.432778 0,-4.198055 1.37583,-5.820833 3.77472,-5.820833 1.27,0 2.36361,0.423333 3.56306,1.340555 l 2.39889,-3.316111 c -1.79917,-1.516944 -3.81,-2.293055 -6.42056,-2.293055 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5026" />   <path-     id="path4272"-     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" />+     d="m 137.80919,16.00926 h -5.57388 v 13.0175 c -0.81139,1.411111 -1.86973,2.187222 -3.03389,2.187222 -1.16417,0 -1.79917,-0.564444 -1.79917,-2.469444 V 16.00926 h -5.57389 v 13.440833 c 0,3.633612 1.72861,5.891389 5.22111,5.891389 2.46945,0 4.30389,-0.987777 5.67972,-2.963333 l 0.24695,2.363611 h 4.83305 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5028" />   <path-     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 158.38283,76.488293 276.03493,252.68766"-     id="path4286" />+     d="m 152.35301,15.480093 c -2.15194,0 -3.98639,1.552222 -4.83305,4.162778 l -0.45861,-3.633611 h -4.86834 v 18.7325 h 5.57389 v -9.278056 c 0.59972,-2.8575 1.55222,-4.586111 3.88056,-4.586111 0.635,0 1.09361,0.105834 1.69333,0.246945 l 0.88195,-5.3975 c -0.59973,-0.176389 -1.16417,-0.246945 -1.86973,-0.246945 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5030" />   <path-     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 157.10433,253.63953 56.2492,-84.19363"-     id="path4288" />+     d="m 166.47791,15.480093 c -2.15195,0 -3.98639,1.552222 -4.83306,4.162778 l -0.45861,-3.633611 h -4.86833 v 18.7325 h 5.57389 v -9.278056 c 0.59972,-2.8575 1.55222,-4.586111 3.88055,-4.586111 0.635,0 1.09361,0.105834 1.69333,0.246945 l 0.88195,-5.3975 c -0.59972,-0.176389 -1.16417,-0.246945 -1.86972,-0.246945 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5032" />   <path-     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"-     d="m 157.27873,253.21481 -80.554303,-118.985 61.356603,-89.60207 12.4395,19.804663"-     id="path4290" />+     d="m 172.69367,42.749608 -0.67028,-3.81 c 3.59833,-0.564444 4.72722,-1.516944 5.75028,-4.198055 h -1.97556 l -5.67972,-18.062223 5.50333,-1.27 3.42195,15.063612 c 1.34055,-8.11389 2.46944,-11.430001 4.72722,-15.063612 l 5.36222,1.763889 c -2.50472,3.386667 -3.66889,6.914445 -5.89139,16.333611 -1.51694,6.561667 -4.48028,8.360834 -10.54805,9.242778 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';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4938" />   <path-     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"-     d="m 177.55693,283.63875 39.6419,55.75247 58.8337,-88.00034"-     id="path4292" />-  <g-     id="text4306"-     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:'Roboto Condensed';-inkscape-font-specification:'Roboto Condensed, ';letter-spacing:0px;word-spacing:0px;opacity:1;fill:#373737;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"-     transform="rotate(0.19415858)"-     aria-label="Gifcurry">-    <path-       id="path4507"-       style="font-size:40px;line-height:1.25"-       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" />-    <path-       id="path4509"-       style="font-size:40px;line-height:1.25"-       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" />-    <path-       id="path4511"-       style="font-size:40px;line-height:1.25"-       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" />-    <path-       id="path4513"-       style="font-size:40px;line-height:1.25"-       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" />-    <path-       id="path4515"-       style="font-size:40px;line-height:1.25"-       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" />-    <path-       id="path4517"-       style="font-size:40px;line-height:1.25"-       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" />-    <path-       id="path4519"-       style="font-size:40px;line-height:1.25"-       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" />-    <path-       id="path4521"-       style="font-size:40px;line-height:1.25"-       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" />-  </g>-  <text-     xml:space="preserve"-     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:'Roboto Condensed';-inkscape-font-specification:'Roboto Condensed, ';letter-spacing:0px;word-spacing:0px;opacity:1;fill:#373737;fill-opacity:0.284153;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"-     x="-160.13664"-     y="380.55551"-     id="text4502"-     transform="rotate(-56.324282)"><tspan-       style="font-size:7.5px;line-height:1.25"-       id="tspan4504"-       x="-160.13664"-       y="380.55551">(C) DAVID LETTIER</tspan></text>-  <text-     xml:space="preserve"-     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;line-height:0%;font-family:'Roboto Condensed';-inkscape-font-specification:'Roboto Condensed, ';letter-spacing:0px;word-spacing:0px;opacity:1;fill:#373737;fill-opacity:1;fill-rule:nonzero;stroke:none;stroke-width:6;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"-     x="236.35316"-     y="-52.491615"-     id="text4505"-     transform="rotate(0.19415858)"><tspan-       id="tspan4503"-       x="236.35316"-       y="-52.491615"-       style="font-size:40px;line-height:1.25">Gifcurry</tspan></text>+     d="m 71.138413,9.6945374 c -6.561667,0 -11.712222,4.5155556 -11.712222,12.8058336 0,8.360834 3.739444,12.841111 11.218333,12.841111 3.069167,0 6.244167,-0.9525 8.784167,-2.575277 V 20.665927 H 70.08008 l 0.564444,4.092222 h 3.175 v 5.538611 c -1.023055,0.564445 -2.116666,0.811389 -3.316111,0.811389 -3.386667,0 -5.08,-2.187222 -5.08,-8.607778 0,-6.067778 2.54,-8.607778 5.961945,-8.607778 1.975555,0 3.316111,0.599722 4.974166,1.869722 L 79.28758,12.693149 C 77.276747,10.92926 74.701469,9.6945374 71.138413,9.6945374 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5042" />+  <path+     d="m 85.938544,6.1314818 c -1.940278,0 -3.280834,1.3758334 -3.280834,3.1750001 0,1.7991671 1.340556,3.1750001 3.280834,3.1750001 1.940278,0 3.316111,-1.375833 3.316111,-3.1750001 0,-1.7991667 -1.375833,-3.1750001 -3.316111,-3.1750001 z m 2.822222,9.8777782 h -5.573889 v 18.7325 h 5.573889 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5044" />+  <path+     d="m 101.68125,12.093426 c 0.81139,0 1.86973,0.176389 2.92806,0.705556 l 1.51694,-3.6336112 c -1.37583,-0.7055556 -3.175,-1.1994445 -5.11527,-1.1994445 -4.797782,0 -7.161393,2.7869447 -7.161393,6.3852777 v 1.658056 h -2.822222 v 3.880555 h 2.822222 V 34.74176 h 5.573889 V 19.889815 h 3.810004 l 0.59972,-3.880555 h -4.409724 v -1.446389 c 0,-1.763889 0.635004,-2.469445 2.257774,-2.469445 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5046" />+  <path+     d="m 113.41828,15.409538 c -5.60917,0 -9.17222,4.162777 -9.17222,10.124722 0,5.926667 3.52778,9.807222 9.27805,9.807222 2.57528,0 4.58611,-0.846666 6.31473,-2.2225 l -2.43417,-3.457222 c -1.34056,0.846667 -2.25778,1.27 -3.59833,1.27 -2.2225,0 -3.70417,-1.27 -3.70417,-5.432778 0,-4.198055 1.37583,-5.820833 3.77472,-5.820833 1.27,0 2.36361,0.423333 3.56306,1.340555 l 2.39889,-3.316111 c -1.79917,-1.516944 -3.81,-2.293055 -6.42056,-2.293055 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5048" />+  <path+     d="m 137.64529,16.00926 h -5.57389 v 13.0175 c -0.81139,1.411111 -1.86972,2.187222 -3.03389,2.187222 -1.16416,0 -1.79916,-0.564444 -1.79916,-2.469444 V 16.00926 h -5.57389 v 13.440833 c 0,3.633612 1.72861,5.891389 5.22111,5.891389 2.46944,0 4.30389,-0.987777 5.67972,-2.963333 l 0.24695,2.363611 h 4.83305 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5050" />+  <path+     d="m 152.18911,15.480093 c -2.15194,0 -3.98639,1.552222 -4.83305,4.162778 l -0.45862,-3.633611 h -4.86833 v 18.7325 h 5.57389 v -9.278056 c 0.59972,-2.8575 1.55222,-4.586111 3.88056,-4.586111 0.635,0 1.09361,0.105834 1.69333,0.246945 l 0.88194,-5.3975 c -0.59972,-0.176389 -1.16416,-0.246945 -1.86972,-0.246945 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5052" />+  <path+     d="m 166.314,15.480093 c -2.15194,0 -3.98638,1.552222 -4.83305,4.162778 L 161.02234,16.00926 H 156.154 v 18.7325 h 5.57389 v -9.278056 c 0.59973,-2.8575 1.55223,-4.586111 3.88056,-4.586111 0.635,0 1.09361,0.105834 1.69333,0.246945 l 0.88195,-5.3975 C 167.584,15.550649 167.01956,15.480093 166.314,15.480093 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;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5054" />+  <path+     d="m 172.85756,42.749608 -0.67028,-3.81 c 3.59834,-0.564444 4.72723,-1.516944 5.75028,-4.198055 h -1.97555 l -5.67973,-18.062223 5.50334,-1.27 3.42194,15.063612 c 1.34056,-8.11389 2.46945,-11.430001 4.72722,-15.063612 l 5.36223,1.763889 c -2.50473,3.386667 -3.66889,6.914445 -5.89139,16.333611 -1.51695,6.561667 -4.48028,8.360834 -10.54806,9.242778 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';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#2e323c;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path5016" />+  <path+     d="m 172.54897,42.749608 -0.67028,-3.81 c 3.59833,-0.564444 4.72722,-1.516944 5.75028,-4.198055 h -1.97556 l -5.67972,-18.062223 5.50333,-1.27 3.42195,15.063612 c 1.34055,-8.11389 2.46944,-11.430001 4.72722,-15.063612 l 5.36222,1.763889 c -2.50472,3.386667 -3.66889,6.914445 -5.89139,16.333611 -1.51694,6.561667 -4.48027,8.360834 -10.54805,9.242778 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';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;display:inline;fill:#444a58;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4934" />+  <path+     d="m 70.993729,9.6945374 c -6.561667,0 -11.712222,4.5155556 -11.712222,12.8058336 0,8.360834 3.739444,12.841111 11.218333,12.841111 3.069167,0 6.244167,-0.9525 8.784167,-2.575277 V 20.665927 h -9.348611 l 0.564444,4.092222 h 3.175 v 5.538611 c -1.023055,0.564445 -2.116666,0.811389 -3.316111,0.811389 -3.386666,0 -5.08,-2.187222 -5.08,-8.607778 0,-6.067778 2.54,-8.607778 5.961945,-8.607778 1.975555,0 3.316111,0.599722 4.974167,1.869722 l 2.928055,-3.069166 C 77.132063,10.92926 74.556785,9.6945374 70.993729,9.6945374 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;fill:#444a58;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4942" />+  <path+     d="m 85.79386,6.1314818 c -1.940278,0 -3.280833,1.3758334 -3.280833,3.1750001 0,1.7991671 1.340555,3.1750001 3.280833,3.1750001 1.940278,0 3.316111,-1.375833 3.316111,-3.1750001 0,-1.7991667 -1.375833,-3.1750001 -3.316111,-3.1750001 z m 2.822222,9.8777782 h -5.573889 v 18.7325 h 5.573889 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;fill:#444a58;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4944" />+  <path+     d="m 101.53657,12.093426 c 0.81139,0 1.86972,0.176389 2.92806,0.705556 l 1.51694,-3.6336112 c -1.37583,-0.7055556 -3.175,-1.1994445 -5.11528,-1.1994445 -4.797775,0 -7.161387,2.7869447 -7.161387,6.3852777 v 1.658056 h -2.822222 v 3.880555 h 2.822222 V 34.74176 h 5.573889 V 19.889815 h 3.809998 l 0.59972,-3.880555 h -4.409718 v -1.446389 c 0,-1.763889 0.635001,-2.469445 2.257778,-2.469445 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;fill:#444a58;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4946" />+  <path+     d="m 113.2736,15.409538 c -5.60917,0 -9.17223,4.162777 -9.17223,10.124722 0,5.926667 3.52778,9.807222 9.27806,9.807222 2.57528,0 4.58611,-0.846666 6.31472,-2.2225 l -2.43417,-3.457222 c -1.34055,0.846667 -2.25777,1.27 -3.59833,1.27 -2.2225,0 -3.70417,-1.27 -3.70417,-5.432778 0,-4.198055 1.37584,-5.820833 3.77473,-5.820833 1.27,0 2.36361,0.423333 3.56305,1.340555 l 2.39889,-3.316111 c -1.79917,-1.516944 -3.81,-2.293055 -6.42055,-2.293055 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;fill:#444a58;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4948" />+  <path+     d="m 137.50061,16.00926 h -5.57389 v 13.0175 c -0.81139,1.411111 -1.86972,2.187222 -3.03389,2.187222 -1.16417,0 -1.79917,-0.564444 -1.79917,-2.469444 V 16.00926 h -5.57388 v 13.440833 c 0,3.633612 1.72861,5.891389 5.22111,5.891389 2.46944,0 4.30389,-0.987777 5.67972,-2.963333 l 0.24694,2.363611 h 4.83306 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;fill:#444a58;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4950" />+  <path+     d="m 152.04443,15.480093 c -2.15195,0 -3.98639,1.552222 -4.83306,4.162778 l -0.45861,-3.633611 h -4.86833 v 18.7325 h 5.57389 v -9.278056 c 0.59972,-2.8575 1.55222,-4.586111 3.88055,-4.586111 0.635,0 1.09361,0.105834 1.69333,0.246945 l 0.88195,-5.3975 c -0.59972,-0.176389 -1.16417,-0.246945 -1.86972,-0.246945 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;fill:#444a58;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4952" />+  <path+     d="m 166.16932,15.480093 c -2.15194,0 -3.98639,1.552222 -4.83306,4.162778 l -0.45861,-3.633611 h -4.86833 v 18.7325 h 5.57389 v -9.278056 c 0.59972,-2.8575 1.55222,-4.586111 3.88055,-4.586111 0.635,0 1.09362,0.105834 1.69334,0.246945 l 0.88194,-5.3975 c -0.59972,-0.176389 -1.16416,-0.246945 -1.86972,-0.246945 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;fill:#444a58;fill-opacity:1;stroke:none;stroke-width:0.26458332px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4954" />+  <path+     id="path4614"+     d="M 20.697583,51.137683 C 14.665366,49.51096 5.9017679,44.57451 2.5850611,32.323578"+     style="display:inline;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" />+  <path+     d="m 18.047816,51.681233 q 0.07334,-0.228402 0.207256,-0.386963 0.133051,-0.155874 0.299898,-0.241612 0.165985,-0.08305 0.354155,-0.09673 0.187308,-0.01099 0.37003,0.04768 0.182721,0.05868 0.328611,0.176663 0.145028,0.120675 0.231628,0.284835 0.08574,0.166848 0.103165,0.371042 0.01656,0.206882 -0.05678,0.435284 -0.07334,0.228402 -0.206393,0.384276 -0.133914,0.158561 -0.299898,0.241612 -0.166848,0.08574 -0.354156,0.09673 -0.18817,0.01368 -0.370892,-0.045 -0.182721,-0.05868 -0.327748,-0.17935 -0.145891,-0.117988 -0.231629,-0.284835 -0.0866,-0.164161 -0.103164,-0.371042 -0.01743,-0.204195 0.05592,-0.432597 z m 0.196156,0.06299 q -0.05695,0.177348 -0.0498,0.339707 0.0063,0.165047 0.06937,0.300906 0.0604,0.134997 0.17663,0.234569 0.113547,0.09871 0.272084,0.149618 0.158538,0.05091 0.311012,0.03762 0.149788,-0.01415 0.280174,-0.08788 0.1277,-0.0746 0.228899,-0.205128 0.100336,-0.127845 0.157286,-0.305192 0.05695,-0.177348 0.05066,-0.342394 -0.0071,-0.16236 -0.06754,-0.297357 -0.06308,-0.135859 -0.17663,-0.234568 -0.116234,-0.09957 -0.274771,-0.150481 -0.158538,-0.05091 -0.308325,-0.03676 -0.152474,0.01329 -0.280174,0.08788 -0.130387,0.07373 -0.230723,0.201578 -0.101199,0.130532 -0.158149,0.307879 z m 0.869193,-0.334468 q 0.115545,0.0371 0.197513,0.104922 0.08197,0.06782 0.129397,0.160118 0.04474,0.09144 0.04957,0.205625 0.004,0.116876 -0.03918,0.25123 -0.04228,0.131667 -0.115375,0.220833 -0.07578,0.0883 -0.168081,0.135733 -0.09316,0.05012 -0.199289,0.05754 -0.106128,0.0074 -0.210924,-0.02623 -0.06986,-0.02243 -0.122842,-0.0513 -0.05566,-0.02973 -0.09348,-0.05966 -0.03868,-0.02724 -0.06037,-0.05199 -0.02524,-0.02293 -0.0334,-0.03444 l 0.113575,-0.141379 q 0.02621,0.02916 0.09099,0.07665 0.06124,0.0493 0.155283,0.07951 0.137041,0.04401 0.259465,-0.02339 0.121561,-0.06471 0.180236,-0.247431 0.0535,-0.166599 0.0047,-0.300831 -0.04965,-0.131545 -0.210872,-0.183317 -0.104796,-0.03365 -0.178876,-0.03373 -0.07763,0.0017 -0.121289,0.0085 l 7.17e-4,-0.177619 q 0.01602,-0.0037 0.05345,-0.0095 0.03387,-0.0039 0.08387,-0.0027 0.04913,0.0039 0.111699,0.01215 0.05902,0.01006 0.123507,0.03077 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4964" />+  <path+     d="m 15.776358,51.246383 q 0.04087,-0.09302 0.118222,-0.163841 0.07363,-0.06937 0.173109,-0.127396 0.09834,-0.05544 0.212181,-0.104065 0.112707,-0.04604 0.224278,-0.0895 0.06322,-0.02463 0.148762,-0.05795 0.08295,-0.03445 0.163011,-0.07635 0.08006,-0.04189 0.142341,-0.08543 0.0597,-0.04467 0.079,-0.0886 l -0.824246,-0.362131 0.08968,-0.204123 1.085214,0.476786 q -0.0042,0.01664 -0.01104,0.03214 -0.0068,0.0155 -0.01249,0.02842 -0.05335,0.121441 -0.137521,0.207767 -0.08788,0.08777 -0.193348,0.152412 -0.105464,0.06464 -0.221889,0.11213 -0.116426,0.04749 -0.227998,0.09095 -0.09184,0.03363 -0.174795,0.06809 -0.08667,0.0359 -0.156708,0.07603 -0.07375,0.04158 -0.127973,0.09482 -0.0568,0.05211 -0.08859,0.124457 -0.03406,0.07751 -0.03206,0.143128 -5.85e-4,0.06448 0.02486,0.118814 0.02545,0.05434 0.07548,0.09481 0.04631,0.04193 0.108326,0.06917 0.07493,0.03292 0.146026,0.0395 0.07109,0.0066 0.132674,-2.79e-4 0.06044,-0.0043 0.107657,-0.02052 0.04608,-0.01366 0.07098,-0.02122 l 0.04393,0.222753 q -0.03461,0.01562 -0.09701,0.03136 -0.06354,0.01832 -0.142891,0.02353 -0.07935,0.0052 -0.169669,-0.0067 -0.09032,-0.01194 -0.183335,-0.0528 -0.281639,-0.123737 -0.363271,-0.316814 -0.08535,-0.191629 0.01909,-0.429342 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4966" />+  <path+     d="m 15.534593,50.607467 q -0.225197,0.436386 -0.499841,0.593186 -0.277152,0.155507 -0.550521,0.01443 -0.273368,-0.141072 -0.304696,-0.455769 -0.03384,-0.315991 0.191361,-0.752377 0.225197,-0.436386 0.502349,-0.591893 0.274644,-0.1568 0.548012,-0.01573 0.273369,0.141072 0.307205,0.457063 0.03133,0.314697 -0.193869,0.751084 z m -0.917916,-0.47369 q -0.07377,0.142954 -0.122222,0.27992 -0.05096,0.135671 -0.06001,0.251682 -0.01156,0.114716 0.02517,0.203542 0.03544,0.09133 0.130743,0.140514 0.0953,0.04918 0.190277,0.02515 0.09368,-0.02153 0.182989,-0.09612 0.0868,-0.07589 0.170362,-0.194735 0.08105,-0.120141 0.154825,-0.263095 0.07377,-0.142954 0.124729,-0.278626 0.04845,-0.136966 0.06001,-0.251682 0.0091,-0.116011 -0.02639,-0.207344 -0.03673,-0.08882 -0.132037,-0.138006 -0.0953,-0.04918 -0.188983,-0.02766 -0.09497,0.02403 -0.181775,0.09992 -0.08931,0.0746 -0.170362,0.194736 -0.08356,0.118847 -0.157333,0.261801 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4968" />+  <path+     d="m 13.671005,50.26862 q -0.169925,-0.0293 -0.34997,-0.02519 -0.181495,0.0065 -0.365821,0.06728 l -0.159849,-0.09562 1.004032,-1.678414 0.225241,0.13474 -0.817134,1.365982 q 0.045,-0.0093 0.105982,-0.01224 0.05856,-0.0044 0.125357,-6.5e-4 0.06438,0.0023 0.134569,0.01144 0.06777,0.0077 0.128774,0.02113 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4970" />+  <path+     d="m 11.853963,47.857966 q 0.06716,-0.09767 0.159609,-0.157403 0.08852,-0.05901 0.199138,-0.07542 0.10669,-0.01569 0.229073,0.02051 0.123982,0.03388 0.25421,0.123424 0.151158,0.103939 0.224697,0.216156 0.06961,0.112944 0.0853,0.219634 0.01569,0.106689 -0.0115,0.201021 -0.02879,0.09666 -0.07676,0.166421 -0.04637,0.06744 -0.108878,0.113511 -0.06643,0.0468 -0.13809,0.0763 -0.07399,0.0279 -0.14928,0.03778 -0.07922,0.0106 -0.15509,0.0064 0.138351,0.286935 -0.03595,0.540415 -0.06076,0.08837 -0.150014,0.14345 -0.09085,0.05741 -0.195214,0.0747 -0.106689,0.01569 -0.22137,-0.01179 -0.116279,-0.02515 -0.227903,-0.10191 -0.130228,-0.08955 -0.196064,-0.193043 -0.06584,-0.103496 -0.0808,-0.206261 -0.01889,-0.102038 0.0067,-0.194044 0.02326,-0.0936 0.07124,-0.16337 0.04637,-0.06744 0.108877,-0.113511 0.05858,-0.04535 0.128643,-0.07252 0.06774,-0.02877 0.139104,-0.03793 0.06904,-0.01075 0.136341,-0.009 -0.184862,-0.318917 0.004,-0.603501 z m 0.846775,0.561707 q 0.02878,-0.04186 0.04681,-0.09796 0.0141,-0.05538 0.0041,-0.120496 -0.01162,-0.06279 -0.05217,-0.12835 -0.04055,-0.06556 -0.128917,-0.126321 -0.07907,-0.05437 -0.153195,-0.07109 -0.07645,-0.01832 -0.137647,-0.009 -0.06279,0.01162 -0.113378,0.04534 -0.05058,0.03372 -0.08417,0.08255 -0.05117,0.07442 -0.06164,0.149417 -0.01047,0.075 0.01423,0.153638 0.02238,0.07704 0.07761,0.156114 0.0513,0.0798 0.129931,0.164694 0.144624,0.01382 0.261927,-0.03225 0.114977,-0.04767 0.196529,-0.166267 z m -1.149041,0.425789 q -0.02399,0.03488 -0.03649,0.08794 -0.01483,0.05145 -0.0063,0.108723 0.007,0.0596 0.04214,0.118029 0.03357,0.06076 0.107987,0.111929 0.07442,0.05117 0.143893,0.06469 0.06715,0.01192 0.125294,-0.0029 0.05654,-0.0125 0.103203,-0.04549 0.04274,-0.03226 0.07152,-0.07412 0.03518,-0.05116 0.05175,-0.115114 0.01657,-0.06395 0.0074,-0.135322 -0.0092,-0.07137 -0.04795,-0.149426 -0.04113,-0.07966 -0.115105,-0.16135 -0.137648,-0.009 -0.248554,0.02774 -0.112506,0.03909 -0.198855,0.164667 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4972" />+  <path+     d="m 9.4441014,46.52114 q 0.1636636,-0.194243 0.3459334,-0.287929 0.180451,-0.09153 0.3705352,-0.09375 0.188266,-6.2e-5 0.381508,0.08157 0.191424,0.08379 0.374876,0.23836 0.09065,0.07638 0.193443,0.177752 0.102457,0.0974 0.172761,0.193539 L 10.061137,48.28103 Q 9.9544628,48.228054 9.8410948,48.143605 9.7237508,48.059495 9.6331037,47.983119 9.4496523,47.828548 9.336423,47.651954 9.2213752,47.477518 9.1913176,47.289813 9.1594416,47.104265 9.2208489,46.908744 9.2804378,46.715382 9.4441014,46.52114 Z m 1.3010156,0.107156 q -0.297839,-0.25095 -0.571894,-0.22353 -0.2758742,0.02958 -0.5068216,0.303677 -0.2309475,0.274098 -0.2114802,0.548833 0.017649,0.276893 0.3154876,0.527844 0.08849,0.07456 0.139947,0.114224 0.0493,0.03785 0.07054,0.05206 L 10.944692,46.80753 q -0.01761,-0.01852 -0.06327,-0.06069 -0.04782,-0.04398 -0.136309,-0.118542 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4974" />+  <path+     d="m 9.3347336,45.317914 q -0.066929,-0.06476 -0.1195953,-0.111799 -0.056657,-0.04697 -0.098986,-0.07222 l -0.3041929,0.314363 q 0.010469,0.02977 0.051229,0.08099 0.036769,0.05129 0.1016701,0.11409 0.042591,0.04121 0.097154,0.08223 0.052535,0.03905 0.1127235,0.05802 0.056198,0.01904 0.1159267,0.01007 0.057766,-0.0069 0.110755,-0.0617 0.098127,-0.101408 0.072477,-0.204772 -0.027613,-0.101336 -0.139161,-0.209275 z m -0.8983159,0.960818 q -0.1135765,-0.109901 -0.1612082,-0.2149 -0.051622,-0.104933 -0.045281,-0.204831 0.00235,-0.09983 0.054682,-0.1945 0.05037,-0.09264 0.1367213,-0.181877 l 0.6378237,-0.65915 q 0.028263,0.0195 0.080732,0.05456 0.048479,0.03513 0.1091264,0.08203 0.060648,0.0469 0.1294736,0.105648 0.06876,0.05475 0.131633,0.115593 0.089239,0.08635 0.1466174,0.177219 0.057378,0.09087 0.07485,0.18239 0.01551,0.09355 -0.01687,0.18789 -0.03238,0.09434 -0.1206928,0.185606 -0.084389,0.08721 -0.1797063,0.11672 -0.097345,0.02755 -0.195478,0.0072 -0.098133,-0.02034 -0.1969215,-0.08059 -0.098789,-0.06025 -0.1880275,-0.1466 -0.028394,-0.02748 -0.054891,-0.06097 -0.02846,-0.03147 -0.048938,-0.06306 -0.024469,-0.03153 -0.040826,-0.05521 -0.016356,-0.02368 -0.022506,-0.03356 l -0.051026,0.05273 q -0.045138,0.04665 -0.078173,0.10108 -0.034998,0.05646 -0.041995,0.116452 -0.00896,0.06202 0.018062,0.127439 0.023031,0.06549 0.096045,0.136137 0.093295,0.09028 0.178018,0.144768 0.080732,0.05456 0.1289487,0.07372 l -0.1288057,0.177756 q -0.052207,-0.0191 -0.151127,-0.08733 -0.1008826,-0.0662 -0.200262,-0.162366 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4976" />+  <path+     d="m 6.9504777,44.676491 q 0.3776384,-0.198203 0.7571358,-0.3444 0.3775854,-0.148274 0.6913119,-0.249237 l 0.1606125,0.174368 Q 8.4331927,44.561615 8.2563731,44.927848 8.0776414,45.292005 7.8491433,45.652122 L 7.6579378,45.444541 q 0.07877,-0.1186 0.1644226,-0.258888 0.081665,-0.140452 0.1614171,-0.28298 0.075765,-0.142691 0.145302,-0.279646 0.069537,-0.136955 0.1200648,-0.248726 -0.1155374,0.04119 -0.2577327,0.09927 -0.1421953,0.05808 -0.288706,0.123961 -0.1504986,0.06572 -0.295261,0.137673 -0.1487503,0.07179 -0.2734096,0.140567 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4978" />+  <path+     d="m 7.690007,43.271166 0.1693753,0.200502 -1.1210824,0.947044 -0.1693753,-0.200501 z m -1.2381414,1.21957 q 0.045531,0.0539 0.043818,0.121845 -0.00569,0.06761 -0.066056,0.118607 -0.060366,0.05099 -0.1258219,0.04348 -0.069433,-0.0078 -0.1149641,-0.06174 -0.045531,-0.0539 -0.039841,-0.12151 0.00171,-0.06795 0.062079,-0.118941 0.060366,-0.051 0.129799,-0.04315 0.065456,0.0075 0.1109869,0.06141 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4980" />+  <path+     d="m 5.9011197,42.875617 q -0.00217,0.06211 0.023657,0.156111 0.025826,0.094 0.091072,0.17911 0.06868,0.08959 0.1520703,0.128793 0.079434,0.03868 0.1685753,0.03435 0.086902,-0.0026 0.179032,-0.04478 0.09213,-0.04218 0.1817236,-0.110856 0.203825,-0.156247 0.2385519,-0.342889 0.032487,-0.184926 -0.096288,-0.352914 -0.065246,-0.08511 -0.1148903,-0.135958 -0.053601,-0.05137 -0.086599,-0.07586 z M 5.1171775,43.476566 4.9216594,43.295733 6.6261739,41.989099 q 0.075102,0.05622 0.1811108,0.152761 0.1060083,0.09654 0.2176131,0.242128 0.1030198,0.13439 0.1428787,0.274525 0.039859,0.140135 0.019843,0.276384 -0.020016,0.136249 -0.099385,0.264656 -0.083326,0.127884 -0.2244353,0.236055 -0.1343901,0.103019 -0.2721368,0.155272 -0.1394637,0.05001 -0.2716076,0.04463 -0.132144,-0.0054 -0.2507715,-0.06736 -0.1203446,-0.06421 -0.2164964,-0.189644 -0.077265,-0.100792 -0.1087647,-0.197551 -0.033217,-0.099 -0.037768,-0.155962 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4982" />+  <path+     d="M 5.188436,40.177027 5.3882578,40.049466 6.0215086,41.041438 4.3729794,42.093819 4.2256765,41.863072 5.6743839,40.938253 Z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4984" />+  <path+     d="m 4.6713638,40.270735 q -0.1698284,0.09529 -0.3229728,0.116489 -0.1556057,0.02258 -0.2848608,-0.01169 -0.129255,-0.03427 -0.2304778,-0.116629 -0.1012228,-0.08236 -0.1633658,-0.193116 -0.1450002,-0.258435 -0.059889,-0.487409 0.08265,-0.227594 0.4124614,-0.412642 0.014768,-0.0083 0.039381,-0.0221 0.022152,-0.01243 0.043223,-0.02101 l 0.4916197,0.876216 Q 4.7875599,39.872223 4.8343146,39.71331 4.8810693,39.554398 4.7609262,39.340266 4.6932595,39.219663 4.6279977,39.149489 4.5588938,39.078234 4.5157273,39.04744 l 0.1740275,-0.139711 q 0.045628,0.02941 0.1296216,0.121438 0.082613,0.08957 0.1571844,0.222474 0.093905,0.167367 0.113727,0.31805 0.01598,0.149603 -0.028436,0.278078 -0.044416,0.128475 -0.1429231,0.235523 -0.1023497,0.105967 -0.2475649,0.187443 z M 4.0528377,39.462486 q -0.1564416,0.08454 -0.2093214,0.221 -0.056722,0.13538 0.021993,0.275673 0.044191,0.07876 0.110711,0.12234 0.062678,0.0425 0.1353801,0.05672 0.072702,0.01422 0.1505047,0.0029 0.076422,-0.01375 0.1470193,-0.04366 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4986" />+  <path+     d="m 3.0888372,38.892274 -0.2476331,-0.497785 0.1970922,-0.09805 0.2476331,0.497784 0.6064377,-0.301685 q 0.098546,-0.04902 0.1567014,-0.09687 0.055628,-0.04659 0.078447,-0.09576 0.020291,-0.04792 0.012826,-0.100945 -0.00747,-0.05302 -0.037634,-0.113669 -0.052795,-0.106126 -0.1069618,-0.157984 -0.057951,-0.05313 -0.080628,-0.07337 l 0.171939,-0.142273 q 0.035286,0.02658 0.10455,0.102445 0.071791,0.07461 0.1283568,0.188318 0.066622,0.133922 0.077769,0.238702 0.00736,0.10351 -0.034464,0.190513 -0.041827,0.087 -0.1290598,0.158768 -0.091017,0.0705 -0.2224116,0.135861 L 2.8393507,39.309536 2.6820186,39.094654 Z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4988" />+  <path+     d="m 2.5751938,37.814121 -0.2274015,-0.507346 0.2008781,-0.09004 0.2274015,0.507346 0.6180863,-0.277037 q 0.1004391,-0.04502 0.1604725,-0.09048 0.057458,-0.04431 0.082237,-0.09253 0.022203,-0.04706 0.016878,-0.100347 -0.00533,-0.05328 -0.033029,-0.115091 -0.048481,-0.108165 -0.1005178,-0.16216 -0.055766,-0.05542 -0.077611,-0.07655 l 0.1775249,-0.135239 q 0.034188,0.02797 0.1003429,0.106569 0.06873,0.07744 0.1206747,0.193332 0.061179,0.136494 0.068101,0.241637 0.00319,0.103723 -0.042102,0.188972 -0.045294,0.08525 -0.1353441,0.153447 -0.09378,0.06677 -0.2276985,0.1268 L 2.3091198,38.22101 2.1605621,37.99997 Z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4990" />+  <path+     d="m 3.4602876,36.154213 0.099655,0.242812 -1.3576587,0.55721 -0.099655,-0.242812 z M 1.9075692,36.93486 q 0.026789,0.06527 0.0043,0.129413 -0.026169,0.0626 -0.099273,0.0926 -0.073105,0.03 -0.1330953,0.0028 -0.063673,-0.02878 -0.090462,-0.09405 -0.026789,-0.06527 -6.204e-4,-0.127873 0.022486,-0.06414 0.095591,-0.09414 0.073105,-0.03 0.1367777,-0.0012 0.059991,0.02724 0.08678,0.09251 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4992" />+  <path+     d="m 2.6303493,36.062347 q -0.182839,0.06701 -0.3374055,0.06355 -0.1572162,-0.0025 -0.2793613,-0.05692 -0.122145,-0.05442 -0.208955,-0.151855 -0.08681,-0.09743 -0.1305152,-0.216675 -0.1019787,-0.278234 0.018522,-0.490726 0.117851,-0.21152 0.4729298,-0.341665 0.015899,-0.0058 0.042397,-0.01554 0.023849,-0.0087 0.046019,-0.01386 l 0.3457563,0.943343 q 0.2088098,-0.09457 0.2802829,-0.244003 0.071473,-0.149435 -0.013024,-0.379971 -0.04759,-0.129842 -0.1008392,-0.209517 -0.05687,-0.08135 -0.09458,-0.118631 l 0.1940617,-0.110204 q 0.040359,0.03631 0.1086205,0.140537 0.06729,0.101581 0.1197361,0.244673 0.066043,0.180189 0.061608,0.332106 -0.00806,0.150237 -0.072372,0.269997 -0.064315,0.119759 -0.1786177,0.209746 -0.1179237,0.08831 -0.2742643,0.145611 z M 2.1484798,35.165886 q -0.1679112,0.05854 -0.2418544,0.184832 -0.077564,0.124615 -0.022204,0.275656 0.031079,0.08479 0.089808,0.138415 0.055108,0.05194 0.1246152,0.07756 0.069508,0.02562 0.1481158,0.02687 0.077637,-0.0014 0.1520963,-0.01968 z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4994" />+  <path+     d="m 1.1680111,34.187928 q -0.010285,-0.03227 -0.017764,-0.07431 -0.011025,-0.04388 -0.015815,-0.08678 -0.00748,-0.04205 -0.0097,-0.07689 -0.00576,-0.03667 -0.00553,-0.05452 l 0.2315164,-0.0264 q -0.00133,0.03301 0.00751,0.107204 0.00529,0.07237 0.039579,0.179923 0.022285,0.06991 0.060703,0.134681 0.034872,0.06294 0.047107,0.08273 l 1.1750584,-0.374559 0.079712,0.25007 -1.3390826,0.426842 Q 1.3607594,34.597467 1.2918717,34.474281 1.220295,34.351952 1.1680111,34.187928 Z"+     style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:2.82222223px;line-height:125%;font-family:Ubuntu;-inkscape-font-specification:Ubuntu;text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#e8e8e8;fill-opacity:1;stroke:none;stroke-width:0.26458335px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"+     id="path4996" /> </svg>
src/data/gui.glade view
@@ -1,531 +1,1230 @@ <?xml version="1.0" encoding="UTF-8"?> <!-- Generated with glade 3.20.0  -Copyright (c) 2017 David Lettier-All rights reserved.--Redistribution and use in source and binary forms, with or without-modification, are permitted provided that the following conditions are met:-    * Redistributions of source code must retain the above copyright-      notice, this list of conditions and the following disclaimer.-    * Redistributions in binary form must reproduce the above copyright-      notice, this list of conditions and the following disclaimer in the-      documentation and/or other materials provided with the distribution.-    * Neither the name of the <organization> nor the-      names of its contributors may be used to endorse or promote products-      derived from this software without specific prior written permission.--THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND-ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED-WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE-DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY-DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES-(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;-LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND-ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT-(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS-SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.--Author: David Lettier---->-<interface>-  <requires lib="gtk+" version="3.10"/>-  <!-- interface-license-type bsd3c -->-  <!-- interface-name Gifcurry -->-  <!-- interface-description Open source video to GIF maker, written in Haskell, that allows overlaid text. -->-  <!-- interface-copyright 2017 David Lettier -->-  <!-- interface-authors David Lettier -->-  <object class="GtkImage" id="about-image">-    <property name="visible">True</property>-    <property name="can_focus">False</property>-    <property name="stock">gtk-about</property>-  </object>-  <object class="GtkImage" id="create-icon">-    <property name="visible">True</property>-    <property name="can_focus">False</property>-    <property name="margin_top">5</property>-    <property name="margin_bottom">5</property>-    <property name="stock">gtk-save</property>-    <property name="icon_size">2</property>-  </object>-  <object class="GtkButton" id="no-button">-    <property name="label">gtk-no</property>-    <property name="visible">True</property>-    <property name="can_focus">True</property>-    <property name="receives_default">True</property>-    <property name="use_stock">True</property>-    <property name="always_show_image">True</property>-  </object>-  <object class="GtkImage" id="open-icon">-    <property name="visible">True</property>-    <property name="can_focus">False</property>-    <property name="margin_top">5</property>-    <property name="margin_bottom">5</property>-    <property name="stock">gtk-open</property>-    <property name="icon_size">2</property>-  </object>-  <object class="GtkWindow" id="gifcurry-window">-    <property name="width_request">400</property>-    <property name="can_focus">False</property>-    <property name="valign">center</property>-    <property name="title">Gifcurry </property>-    <property name="resizable">False</property>-    <property name="window_position">mouse</property>-    <property name="default_width">400</property>-    <property name="icon">gifcurry-icon.png</property>-    <property name="gravity">center</property>-    <child>-      <object class="GtkBox" id="app-box">-        <property name="visible">True</property>-        <property name="can_focus">False</property>-        <property name="orientation">vertical</property>-        <child>-          <object class="GtkBox" id="file-input-about-box">-            <property name="visible">True</property>-            <property name="can_focus">False</property>-            <child>-              <object class="GtkFileChooserButton" id="input-file-button">-                <property name="visible">True</property>-                <property name="can_focus">False</property>-                <property name="tooltip_text" translatable="yes">Select an input file.</property>-                <property name="do_overwrite_confirmation">True</property>-                <property name="local_only">False</property>-                <property name="title" translatable="yes">Gifcurry - Select an Input File</property>-              </object>-              <packing>-                <property name="expand">True</property>-                <property name="fill">True</property>-                <property name="position">0</property>-              </packing>-            </child>-            <child>-              <object class="GtkButton" id="about-button">-                <property name="visible">True</property>-                <property name="can_focus">True</property>-                <property name="receives_default">True</property>-                <property name="tooltip_text" translatable="yes">Information about Gifcurry.</property>-                <property name="image">about-image</property>-                <property name="always_show_image">True</property>-              </object>-              <packing>-                <property name="expand">False</property>-                <property name="fill">False</property>-                <property name="pack_type">end</property>-                <property name="position">1</property>-              </packing>-            </child>-          </object>-          <packing>-            <property name="expand">False</property>-            <property name="fill">False</property>-            <property name="position">0</property>-          </packing>-        </child>-        <child>-          <object class="GtkBox" id="start-duration-box">-            <property name="visible">True</property>-            <property name="can_focus">False</property>-            <property name="homogeneous">True</property>-            <child>-              <object class="GtkEntry" id="start-time-text-entry">-                <property name="visible">True</property>-                <property name="can_focus">True</property>-                <property name="tooltip_text" translatable="yes">Start time.</property>-                <property name="shadow_type">none</property>-                <property name="primary_icon_stock">gtk-media-play</property>-                <property name="placeholder_text" translatable="yes">Start Time (seconds)</property>-                <property name="input_purpose">number</property>-              </object>-              <packing>-                <property name="expand">False</property>-                <property name="fill">True</property>-                <property name="position">0</property>-              </packing>-            </child>-            <child>-              <object class="GtkEntry" id="duration-text-entry">-                <property name="visible">True</property>-                <property name="can_focus">True</property>-                <property name="tooltip_text" translatable="yes">Duration.</property>-                <property name="width_chars">0</property>-                <property name="primary_icon_stock">gtk-media-stop</property>-                <property name="placeholder_text" translatable="yes">Duration (seconds)</property>-                <property name="input_purpose">number</property>-              </object>-              <packing>-                <property name="expand">False</property>-                <property name="fill">True</property>-                <property name="position">1</property>-              </packing>-            </child>-          </object>-          <packing>-            <property name="expand">False</property>-            <property name="fill">True</property>-            <property name="position">2</property>-          </packing>-        </child>-        <child>-          <object class="GtkBox" id="width-quality-box">-            <property name="visible">True</property>-            <property name="can_focus">False</property>-            <property name="homogeneous">True</property>-            <child>-              <object class="GtkEntry" id="width-text-entry">-                <property name="visible">True</property>-                <property name="can_focus">True</property>-                <property name="tooltip_text" translatable="yes">GIF width in px.</property>-                <property name="shadow_type">none</property>-                <property name="primary_icon_stock">gtk-justify-fill</property>-                <property name="placeholder_text" translatable="yes">Width (pixels)</property>-                <property name="input_purpose">number</property>-              </object>-              <packing>-                <property name="expand">False</property>-                <property name="fill">True</property>-                <property name="position">0</property>-              </packing>-            </child>-            <child>-              <object class="GtkEntry" id="quality-text-entry">-                <property name="visible">True</property>-                <property name="can_focus">True</property>-                <property name="tooltip_text" translatable="yes">GIF quality from 1 to 100.</property>-                <property name="max_length">3</property>-                <property name="width_chars">3</property>-                <property name="max_width_chars">3</property>-                <property name="primary_icon_stock">gtk-preferences</property>-                <property name="placeholder_text" translatable="yes">Quality [1-100]</property>-                <property name="input_purpose">number</property>-              </object>-              <packing>-                <property name="expand">True</property>-                <property name="fill">True</property>-                <property name="position">1</property>-              </packing>-            </child>-          </object>-          <packing>-            <property name="expand">True</property>-            <property name="fill">True</property>-            <property name="position">3</property>-          </packing>-        </child>-        <child>-          <object class="GtkFontButton" id="font-chooser-button">-            <property name="name">font_chooser_button</property>-            <property name="visible">True</property>-            <property name="can_focus">True</property>-            <property name="receives_default">True</property>-            <property name="tooltip_text" translatable="yes">Choose your desired font for the bottom and top text.</property>-            <property name="font">Sans 12</property>-            <property name="preview_text">I love making GIFs with Gifcurry!</property>-            <property name="title" translatable="yes">Gifcurry - Choose a Font</property>-            <property name="use_font">True</property>-            <property name="show_style">False</property>-            <property name="show_size">False</property>-          </object>-          <packing>-            <property name="expand">True</property>-            <property name="fill">True</property>-            <property name="position">4</property>-          </packing>-        </child>-        <child>-          <object class="GtkEntry" id="top-text-text-entry">-            <property name="visible">True</property>-            <property name="can_focus">True</property>-            <property name="tooltip_text" translatable="yes">Optional top text.</property>-            <property name="primary_icon_stock">gtk-italic</property>-            <property name="placeholder_text" translatable="yes">Optional top text.</property>-          </object>-          <packing>-            <property name="expand">True</property>-            <property name="fill">True</property>-            <property name="position">5</property>-          </packing>-        </child>-        <child>-          <object class="GtkEntry" id="bottom-text-text-entry">-            <property name="visible">True</property>-            <property name="can_focus">True</property>-            <property name="tooltip_text" translatable="yes">Optional bottom text.</property>-            <property name="primary_icon_stock">gtk-italic</property>-            <property name="secondary_icon_tooltip_text" translatable="yes">Text will show up at the bottom of the GIF.</property>-            <property name="placeholder_text" translatable="yes">Optional bottom text.</property>-          </object>-          <packing>-            <property name="expand">False</property>-            <property name="fill">True</property>-            <property name="position">6</property>-          </packing>-        </child>-        <child>-          <object class="GtkSeparator" id="separator-1">-            <property name="visible">True</property>-            <property name="can_focus">False</property>-          </object>-          <packing>-            <property name="expand">False</property>-            <property name="fill">True</property>-            <property name="position">7</property>-          </packing>-        </child>-        <child>-          <object class="GtkFileChooserButton" id="output-file-path-button">-            <property name="visible">True</property>-            <property name="can_focus">False</property>-            <property name="tooltip_text" translatable="yes">Select an output folder.</property>-            <property name="action">select-folder</property>-            <property name="title" translatable="yes">Gifcurry - Select an Output Folder</property>-          </object>-          <packing>-            <property name="expand">True</property>-            <property name="fill">True</property>-            <property name="position">8</property>-          </packing>-        </child>-        <child>-          <object class="GtkEntry" id="output-file-name-text-entry">-            <property name="visible">True</property>-            <property name="can_focus">True</property>-            <property name="tooltip_text" translatable="yes">Output file name.</property>-            <property name="shadow_type">none</property>-            <property name="primary_icon_stock">gtk-file</property>-            <property name="placeholder_text" translatable="yes">Output file name.</property>-          </object>-          <packing>-            <property name="expand">False</property>-            <property name="fill">True</property>-            <property name="position">9</property>-          </packing>-        </child>-        <child>-          <object class="GtkBox" id="frame-images-box">-            <property name="visible">True</property>-            <property name="can_focus">False</property>-            <child>-              <object class="GtkImage" id="first-frame-image">-                <property name="visible">True</property>-                <property name="can_focus">False</property>-                <property name="tooltip_text" translatable="yes">First frame preview.</property>-                <property name="halign">center</property>-                <property name="valign">center</property>-                <property name="stock">gtk-discard</property>-                <property name="pixel_size">0</property>-                <property name="icon_size">0</property>-              </object>-              <packing>-                <property name="expand">True</property>-                <property name="fill">True</property>-                <property name="position">0</property>-              </packing>-            </child>-            <child>-              <object class="GtkImage" id="last-frame-image">-                <property name="visible">True</property>-                <property name="can_focus">False</property>-                <property name="tooltip_text" translatable="yes">Last frame preview.</property>-                <property name="halign">center</property>-                <property name="valign">center</property>-                <property name="stock">gtk-discard</property>-                <property name="pixel_size">0</property>-                <property name="icon_size">0</property>-              </object>-              <packing>-                <property name="expand">True</property>-                <property name="fill">True</property>-                <property name="pack_type">end</property>-                <property name="position">1</property>-              </packing>-            </child>-            <child>-              <object class="GtkSeparator" id="separator-2">-                <property name="visible">True</property>-                <property name="can_focus">False</property>-                <property name="orientation">vertical</property>-              </object>-              <packing>-                <property name="expand">False</property>-                <property name="fill">True</property>-                <property name="position">2</property>-              </packing>-            </child>-          </object>-          <packing>-            <property name="expand">True</property>-            <property name="fill">True</property>-            <property name="position">10</property>-          </packing>-        </child>-        <child>-          <object class="GtkBox" id="save-open-box">-            <property name="visible">True</property>-            <property name="can_focus">False</property>-            <property name="homogeneous">True</property>-            <child>-              <object class="GtkButton" id="create-button">-                <property name="label">Create</property>-                <property name="visible">True</property>-                <property name="can_focus">True</property>-                <property name="receives_default">True</property>-                <property name="tooltip_text" translatable="yes">Create the GIF.</property>-                <property name="image">create-icon</property>-                <property name="relief">half</property>-                <property name="image_position">top</property>-                <property name="always_show_image">True</property>-              </object>-              <packing>-                <property name="expand">True</property>-                <property name="fill">True</property>-                <property name="position">0</property>-              </packing>-            </child>-            <child>-              <object class="GtkButton" id="open-button">-                <property name="label" translatable="yes">Open</property>-                <property name="visible">True</property>-                <property name="can_focus">True</property>-                <property name="receives_default">True</property>-                <property name="tooltip_text" translatable="yes">Open the GIF.</property>-                <property name="image">open-icon</property>-                <property name="relief">half</property>-                <property name="image_position">top</property>-                <property name="always_show_image">True</property>-              </object>-              <packing>-                <property name="expand">True</property>-                <property name="fill">True</property>-                <property name="pack_type">end</property>-                <property name="position">1</property>-              </packing>-            </child>-          </object>-          <packing>-            <property name="expand">False</property>-            <property name="fill">True</property>-            <property name="position">11</property>-          </packing>-        </child>-        <child>-          <object class="GtkBox" id="link-box">-            <property name="visible">True</property>-            <property name="can_focus">False</property>-            <property name="homogeneous">True</property>-            <child>-              <object class="GtkLinkButton" id="giphy-link-button">-                <property name="label" translatable="yes">button</property>-                <property name="visible">True</property>-                <property name="can_focus">True</property>-                <property name="receives_default">True</property>-                <property name="tooltip_text" translatable="yes">https://giphy.com/create/upload</property>-                <property name="halign">end</property>-                <property name="uri">https://giphy.com/create/upload</property>-              </object>-              <packing>-                <property name="expand">True</property>-                <property name="fill">True</property>-                <property name="position">0</property>-              </packing>-            </child>-            <child>-              <object class="GtkSeparator" id="separator-3">-                <property name="visible">True</property>-                <property name="sensitive">False</property>-                <property name="can_focus">False</property>-                <property name="double_buffered">False</property>-                <property name="halign">baseline</property>-                <property name="valign">baseline</property>-                <property name="orientation">vertical</property>-              </object>-              <packing>-                <property name="expand">False</property>-                <property name="fill">False</property>-                <property name="position">1</property>-              </packing>-            </child>-            <child>-              <object class="GtkLinkButton" id="imgur-link-button">-                <property name="label" translatable="yes">button</property>-                <property name="visible">True</property>-                <property name="can_focus">True</property>-                <property name="receives_default">True</property>-                <property name="tooltip_text" translatable="yes">https://www.imgur.com/</property>-                <property name="halign">start</property>-                <property name="uri">https://imgur.com/upload</property>-              </object>-              <packing>-                <property name="expand">True</property>-                <property name="fill">True</property>-                <property name="pack_type">end</property>-                <property name="position">2</property>-              </packing>-            </child>-          </object>-          <packing>-            <property name="expand">False</property>-            <property name="fill">True</property>-            <property name="position">12</property>-          </packing>-        </child>-        <child>-          <object class="GtkBox" id="status-box">-            <property name="visible">True</property>-            <property name="sensitive">False</property>-            <property name="can_focus">False</property>-            <property name="hexpand">True</property>-            <property name="vexpand">True</property>-            <property name="baseline_position">top</property>-            <child>-              <object class="GtkEntry" id="status-text-entry">-                <property name="visible">True</property>-                <property name="sensitive">False</property>-                <property name="app_paintable">True</property>-                <property name="can_focus">False</property>-                <property name="hexpand">True</property>-                <property name="vexpand">True</property>-                <property name="editable">False</property>-                <property name="has_frame">False</property>-                <property name="text" translatable="yes">Ready.</property>-                <property name="shadow_type">none</property>-                <property name="caps_lock_warning">False</property>-                <property name="primary_icon_stock">gtk-about</property>-                <property name="secondary_icon_activatable">False</property>-                <property name="secondary_icon_sensitive">False</property>-              </object>-              <packing>-                <property name="expand">True</property>-                <property name="fill">True</property>-                <property name="position">0</property>-              </packing>-            </child>-          </object>-          <packing>-            <property name="expand">True</property>-            <property name="fill">True</property>-            <property name="position">13</property>-          </packing>-        </child>-      </object>-    </child>-    <child>-      <placeholder/>-    </child>-  </object>-  <object class="GtkAboutDialog" id="about-dialog">-    <property name="can_focus">False</property>-    <property name="title" translatable="yes">Gifcurry - About</property>-    <property name="window_position">center</property>-    <property name="destroy_with_parent">True</property>-    <property name="icon_name">dialog-information</property>-    <property name="type_hint">dialog</property>-    <property name="skip_taskbar_hint">True</property>-    <property name="gravity">center</property>-    <property name="transient_for">gifcurry-window</property>-    <property name="attached_to">gifcurry-window</property>-    <property name="program_name">Gifcurry</property>-    <property name="version">2.3.0.0</property>+Copyright (c) 2016 David Lettier+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:+    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.+    * Redistributions in binary form must reproduce the above copyright+      notice, this list of conditions and the following disclaimer in the+      documentation and/or other materials provided with the distribution.+    * Neither the name of the <organization> nor the+      names of its contributors may be used to endorse or promote products+      derived from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+DISCLAIMED. IN NO EVENT SHALL <COPYRIGHT HOLDER> BE LIABLE FOR ANY+DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND+ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.++Author: David Lettier++-->+<interface>+  <requires lib="gtk+" version="3.10"/>+  <!-- interface-license-type bsd3c -->+  <!-- interface-name Gifcurry -->+  <!-- interface-description Your open source video to GIF maker built with Haskell. -->+  <!-- interface-copyright 2016 David Lettier -->+  <!-- interface-authors David Lettier -->+  <object class="GtkImage" id="about-image">+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="stock">gtk-about</property>+    <property name="icon_size">3</property>+  </object>+  <object class="GtkAdjustment" id="bottom-crop-adjustment">+    <property name="upper">100</property>+    <property name="step_increment">1</property>+  </object>+  <object class="GtkImage" id="crop-toggle-button-image">+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="stock">gtk-cut</property>+  </object>+  <object class="GtkAdjustment" id="duration-time-adjustment">+    <property name="step_increment">1</property>+  </object>+  <object class="GtkImage" id="gif-icon-image">+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="stock">gtk-dnd-multiple</property>+  </object>+  <object class="GtkBox" id="images-preview-box">+    <property name="visible">True</property>+    <property name="app_paintable">True</property>+    <property name="can_focus">False</property>+    <property name="hexpand">True</property>+    <property name="vexpand">True</property>+    <property name="orientation">vertical</property>+    <child>+      <object class="GtkBox" id="first-frame-preview-box">+        <property name="visible">True</property>+        <property name="app_paintable">True</property>+        <property name="can_focus">False</property>+        <child>+          <object class="GtkOverlay" id="first-frame-preview-image-overlay">+            <property name="visible">True</property>+            <property name="app_paintable">True</property>+            <property name="can_focus">False</property>+            <child>+              <object class="GtkImage" id="first-frame-image">+                <property name="visible">True</property>+                <property name="app_paintable">True</property>+                <property name="can_focus">False</property>+                <property name="tooltip_text" translatable="yes">This is the first frame of the GIF.</property>+                <property name="hexpand">True</property>+                <property name="stock">gtk-missing-image</property>+                <property name="pixel_size">0</property>+              </object>+              <packing>+                <property name="index">-1</property>+              </packing>+            </child>+            <child type="overlay">+              <object class="GtkDrawingArea" id="first-frame-preview-image-drawing-area">+                <property name="visible">True</property>+                <property name="can_focus">False</property>+                <property name="hexpand">True</property>+              </object>+              <packing>+                <property name="index">1</property>+              </packing>+            </child>+          </object>+          <packing>+            <property name="expand">True</property>+            <property name="fill">True</property>+            <property name="position">0</property>+          </packing>+        </child>+        <child>+          <object class="GtkSeparator" id="first-last-frame-preview-image-separator">+            <property name="visible">True</property>+            <property name="sensitive">False</property>+            <property name="can_focus">False</property>+            <property name="orientation">vertical</property>+          </object>+          <packing>+            <property name="expand">False</property>+            <property name="fill">True</property>+            <property name="position">1</property>+          </packing>+        </child>+        <child>+          <object class="GtkOverlay" id="last-frame-preview-image-overlay">+            <property name="visible">True</property>+            <property name="app_paintable">True</property>+            <property name="can_focus">False</property>+            <child>+              <object class="GtkImage" id="last-frame-image">+                <property name="visible">True</property>+                <property name="app_paintable">True</property>+                <property name="can_focus">False</property>+                <property name="tooltip_text" translatable="yes">This is the last frame of the GIF.</property>+                <property name="hexpand">True</property>+                <property name="stock">gtk-missing-image</property>+                <property name="pixel_size">0</property>+              </object>+              <packing>+                <property name="index">-1</property>+              </packing>+            </child>+            <child type="overlay">+              <object class="GtkDrawingArea" id="last-frame-preview-image-drawing-area">+                <property name="visible">True</property>+                <property name="can_focus">False</property>+                <property name="hexpand">True</property>+              </object>+              <packing>+                <property name="index">1</property>+              </packing>+            </child>+          </object>+          <packing>+            <property name="expand">True</property>+            <property name="fill">True</property>+            <property name="position">2</property>+          </packing>+        </child>+      </object>+      <packing>+        <property name="expand">False</property>+        <property name="fill">True</property>+        <property name="position">1</property>+      </packing>+    </child>+    <child>+      <object class="GtkBox" id="last-frame-preview-box">+        <property name="visible">True</property>+        <property name="can_focus">False</property>+        <property name="homogeneous">True</property>+        <child>+          <object class="GtkButton" id="first-frame-preview-label-button">+            <property name="label" translatable="yes">First Frame</property>+            <property name="visible">True</property>+            <property name="sensitive">False</property>+            <property name="can_focus">False</property>+            <property name="receives_default">False</property>+            <property name="tooltip_text" translatable="yes">Last Frame</property>+            <property name="opacity">0.98999999999999999</property>+            <property name="hexpand">True</property>+            <property name="vexpand">True</property>+          </object>+          <packing>+            <property name="expand">True</property>+            <property name="fill">True</property>+            <property name="position">0</property>+          </packing>+        </child>+        <child>+          <object class="GtkButton" id="last-frame-preview-label-button">+            <property name="label" translatable="yes">Last Frame</property>+            <property name="visible">True</property>+            <property name="sensitive">False</property>+            <property name="can_focus">False</property>+            <property name="focus_on_click">False</property>+            <property name="receives_default">False</property>+            <property name="tooltip_text" translatable="yes">First Frame</property>+            <property name="hexpand">True</property>+            <property name="vexpand">True</property>+          </object>+          <packing>+            <property name="expand">True</property>+            <property name="fill">True</property>+            <property name="pack_type">end</property>+            <property name="position">1</property>+          </packing>+        </child>+      </object>+      <packing>+        <property name="expand">True</property>+        <property name="fill">True</property>+        <property name="pack_type">end</property>+        <property name="position">4</property>+      </packing>+    </child>+  </object>+  <object class="GtkAdjustment" id="left-crop-adjustment">+    <property name="upper">100</property>+    <property name="step_increment">1</property>+  </object>+  <object class="GtkButton" id="no-button">+    <property name="label">gtk-no</property>+    <property name="visible">True</property>+    <property name="can_focus">True</property>+    <property name="receives_default">True</property>+    <property name="use_stock">True</property>+    <property name="always_show_image">True</property>+  </object>+  <object class="GtkImage" id="open-icon">+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="margin_top">5</property>+    <property name="margin_bottom">5</property>+    <property name="stock">gtk-open</property>+    <property name="icon_size">2</property>+  </object>+  <object class="GtkAdjustment" id="quality-percent-adjustment">+    <property name="lower">1</property>+    <property name="upper">100</property>+    <property name="value">100</property>+    <property name="step_increment">1</property>+  </object>+  <object class="GtkAdjustment" id="right-crop-adjustment">+    <property name="upper">100</property>+    <property name="step_increment">1</property>+  </object>+  <object class="GtkImage" id="save-icon">+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="margin_top">5</property>+    <property name="margin_bottom">5</property>+    <property name="stock">gtk-save</property>+    <property name="icon_size">2</property>+  </object>+  <object class="GtkImage" id="save-toggle-button-image">+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="stock">gtk-save</property>+  </object>+  <object class="GtkAdjustment" id="start-time-adjustment">+    <property name="step_increment">1</property>+  </object>+  <object class="GtkImage" id="top-bottom-text-toggle-button-image">+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="stock">gtk-italic</property>+  </object>+  <object class="GtkAdjustment" id="top-crop-adjustment">+    <property name="upper">100</property>+    <property name="step_increment">1</property>+  </object>+  <object class="GtkImage" id="upload-toggle-button-image">+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="stock">gtk-sort-descending</property>+  </object>+  <object class="GtkImage" id="video-icon-image">+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="stock">gtk-media-record</property>+  </object>+  <object class="GtkBox" id="video-preview-box">+    <property name="visible">True</property>+    <property name="app_paintable">True</property>+    <property name="can_focus">False</property>+    <property name="tooltip_text" translatable="yes">This is a video preview of the GIF.</property>+    <property name="hexpand">True</property>+    <property name="vexpand">True</property>+    <property name="orientation">vertical</property>+    <child>+      <object class="GtkOverlay" id="video-preview-overlay">+        <property name="visible">True</property>+        <property name="app_paintable">True</property>+        <property name="can_focus">False</property>+        <child>+          <object class="GtkBox" id="video-preview-overlay-child-box">+            <property name="visible">True</property>+            <property name="app_paintable">True</property>+            <property name="can_focus">False</property>+            <property name="orientation">vertical</property>+            <child>+              <placeholder/>+            </child>+          </object>+          <packing>+            <property name="index">-1</property>+          </packing>+        </child>+        <child type="overlay">+          <object class="GtkDrawingArea" id="video-preview-drawing-area">+            <property name="visible">True</property>+            <property name="can_focus">False</property>+          </object>+        </child>+      </object>+      <packing>+        <property name="expand">True</property>+        <property name="fill">True</property>+        <property name="position">0</property>+      </packing>+    </child>+  </object>+  <object class="GtkAdjustment" id="width-size-adjustment">+    <property name="lower">1</property>+    <property name="upper">3840</property>+    <property name="value">500</property>+    <property name="step_increment">1</property>+  </object>+  <object class="GtkImage" id="width-toggle-button-image">+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="stock">gtk-justify-fill</property>+  </object>+  <object class="GtkWindow" id="gifcurry-window">+    <property name="width_request">900</property>+    <property name="visible">True</property>+    <property name="can_focus">False</property>+    <property name="title">Gifcurry </property>+    <property name="window_position">mouse</property>+    <property name="default_width">1000</property>+    <property name="icon">gifcurry-icon.svg</property>+    <property name="gravity">center</property>+    <child>+      <object class="GtkBox" id="app-box">+        <property name="visible">True</property>+        <property name="app_paintable">True</property>+        <property name="can_focus">False</property>+        <property name="hexpand">True</property>+        <property name="vexpand">True</property>+        <property name="orientation">vertical</property>+        <child>+          <object class="GtkBox" id="in-file-chooser-button-about-button-box">+            <property name="visible">True</property>+            <property name="can_focus">False</property>+            <child>+              <object class="GtkFileChooserButton" id="in-file-chooser-button">+                <property name="name">in-file-chooser-button</property>+                <property name="visible">True</property>+                <property name="can_focus">False</property>+                <property name="tooltip_text" translatable="yes">Where is the video you want to make a GIF from?</property>+                <property name="hexpand">True</property>+                <property name="do_overwrite_confirmation">True</property>+                <property name="title" translatable="yes">Gifcurry - Select an Input Video</property>+              </object>+              <packing>+                <property name="expand">True</property>+                <property name="fill">True</property>+                <property name="position">0</property>+              </packing>+            </child>+            <child>+              <object class="GtkButton" id="about-button">+                <property name="visible">True</property>+                <property name="can_focus">True</property>+                <property name="receives_default">True</property>+                <property name="tooltip_text" translatable="yes">What is Gifcurry?</property>+                <property name="image">about-image</property>+                <property name="always_show_image">True</property>+              </object>+              <packing>+                <property name="expand">False</property>+                <property name="fill">False</property>+                <property name="pack_type">end</property>+                <property name="position">1</property>+              </packing>+            </child>+          </object>+          <packing>+            <property name="expand">False</property>+            <property name="fill">False</property>+            <property name="position">1</property>+          </packing>+        </child>+        <child>+          <object class="GtkBox" id="sidebar-controls-preview-box">+            <property name="visible">True</property>+            <property name="app_paintable">True</property>+            <property name="can_focus">False</property>+            <property name="hexpand">True</property>+            <property name="vexpand">True</property>+            <child>+              <object class="GtkBox" id="sidebar-controls-box">+                <property name="width_request">400</property>+                <property name="visible">True</property>+                <property name="can_focus">False</property>+                <property name="vexpand">True</property>+                <property name="orientation">vertical</property>+                <child>+                  <object class="GtkToggleButton" id="width-quality-percent-toggle-button">+                    <property name="label" translatable="yes"> Width &amp; Quality</property>+                    <property name="name">width-quality-percent-toggle-button</property>+                    <property name="visible">True</property>+                    <property name="can_focus">True</property>+                    <property name="focus_on_click">False</property>+                    <property name="receives_default">True</property>+                    <property name="image">width-toggle-button-image</property>+                    <property name="always_show_image">True</property>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">0</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkBox" id="width-quality-percent-box">+                    <property name="can_focus">False</property>+                    <property name="homogeneous">True</property>+                    <child>+                      <object class="GtkSpinButton" id="width-size-spin-button">+                        <property name="name">width-size-spin-button</property>+                        <property name="visible">True</property>+                        <property name="can_focus">True</property>+                        <property name="tooltip_text" translatable="yes">How wide is the GIF?</property>+                        <property name="text" translatable="yes">500</property>+                        <property name="progress_fraction">0.13</property>+                        <property name="primary_icon_stock">gtk-justify-fill</property>+                        <property name="primary_icon_activatable">False</property>+                        <property name="secondary_icon_activatable">False</property>+                        <property name="secondary_icon_sensitive">False</property>+                        <property name="placeholder_text" translatable="yes">Width Size (pixels)</property>+                        <property name="input_purpose">number</property>+                        <property name="adjustment">width-size-adjustment</property>+                        <property name="climb_rate">2</property>+                        <property name="snap_to_ticks">True</property>+                        <property name="numeric">True</property>+                        <property name="update_policy">if-valid</property>+                        <property name="value">500</property>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">2</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkSpinButton" id="quality-percent-spin-button">+                        <property name="name">quality-percent-spin-button</property>+                        <property name="visible">True</property>+                        <property name="can_focus">True</property>+                        <property name="tooltip_text" translatable="yes">What is the quality?</property>+                        <property name="max_length">3</property>+                        <property name="width_chars">3</property>+                        <property name="text" translatable="yes">100</property>+                        <property name="progress_fraction">1</property>+                        <property name="primary_icon_stock">gtk-select-color</property>+                        <property name="primary_icon_activatable">False</property>+                        <property name="secondary_icon_activatable">False</property>+                        <property name="secondary_icon_sensitive">False</property>+                        <property name="placeholder_text" translatable="yes">Quality Percent</property>+                        <property name="input_purpose">number</property>+                        <property name="adjustment">quality-percent-adjustment</property>+                        <property name="climb_rate">2</property>+                        <property name="snap_to_ticks">True</property>+                        <property name="numeric">True</property>+                        <property name="update_policy">if-valid</property>+                        <property name="value">100</property>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">3</property>+                      </packing>+                    </child>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">1</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkSeparator" id="width-quality-crop-separator">+                    <property name="visible">True</property>+                    <property name="can_focus">False</property>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">2</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkToggleButton" id="crop-toggle-button">+                    <property name="label" translatable="yes"> Crop</property>+                    <property name="name">crop-toggle-button</property>+                    <property name="visible">True</property>+                    <property name="can_focus">True</property>+                    <property name="focus_on_click">False</property>+                    <property name="receives_default">True</property>+                    <property name="image">crop-toggle-button-image</property>+                    <property name="always_show_image">True</property>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">3</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkBox" id="crop-spin-buttons-box">+                    <property name="can_focus">False</property>+                    <property name="orientation">vertical</property>+                    <child>+                      <object class="GtkBox" id="left-right-crop-spin-buttons-box">+                        <property name="visible">True</property>+                        <property name="can_focus">False</property>+                        <property name="homogeneous">True</property>+                        <child>+                          <object class="GtkSpinButton" id="left-crop-spin-button">+                            <property name="visible">True</property>+                            <property name="can_focus">True</property>+                            <property name="tooltip_text" translatable="yes">How much to crop from the left?</property>+                            <property name="primary_icon_stock">gtk-goto-last</property>+                            <property name="primary_icon_activatable">False</property>+                            <property name="secondary_icon_activatable">False</property>+                            <property name="secondary_icon_sensitive">False</property>+                            <property name="placeholder_text" translatable="yes">Left Crop</property>+                            <property name="input_purpose">digits</property>+                            <property name="adjustment">left-crop-adjustment</property>+                            <property name="climb_rate">2</property>+                            <property name="digits">2</property>+                            <property name="numeric">True</property>+                            <property name="update_policy">if-valid</property>+                          </object>+                          <packing>+                            <property name="expand">False</property>+                            <property name="fill">True</property>+                            <property name="position">0</property>+                          </packing>+                        </child>+                        <child>+                          <object class="GtkSpinButton" id="right-crop-spin-button">+                            <property name="visible">True</property>+                            <property name="can_focus">True</property>+                            <property name="tooltip_text" translatable="yes">How much to crop from the right?</property>+                            <property name="primary_icon_stock">gtk-goto-first</property>+                            <property name="primary_icon_activatable">False</property>+                            <property name="secondary_icon_activatable">False</property>+                            <property name="secondary_icon_sensitive">False</property>+                            <property name="placeholder_text" translatable="yes">Right Crop</property>+                            <property name="input_purpose">digits</property>+                            <property name="adjustment">right-crop-adjustment</property>+                            <property name="climb_rate">2</property>+                            <property name="digits">2</property>+                            <property name="numeric">True</property>+                            <property name="update_policy">if-valid</property>+                          </object>+                          <packing>+                            <property name="expand">False</property>+                            <property name="fill">True</property>+                            <property name="position">1</property>+                          </packing>+                        </child>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">0</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkBox" id="top-bottom-crop-spin-buttons-box">+                        <property name="visible">True</property>+                        <property name="can_focus">False</property>+                        <property name="homogeneous">True</property>+                        <child>+                          <object class="GtkSpinButton" id="top-crop-spin-button">+                            <property name="visible">True</property>+                            <property name="can_focus">True</property>+                            <property name="tooltip_text" translatable="yes">How much to crop from the top?</property>+                            <property name="primary_icon_stock">gtk-goto-bottom</property>+                            <property name="primary_icon_activatable">False</property>+                            <property name="secondary_icon_activatable">False</property>+                            <property name="secondary_icon_sensitive">False</property>+                            <property name="placeholder_text" translatable="yes">Top Crop</property>+                            <property name="input_purpose">digits</property>+                            <property name="adjustment">top-crop-adjustment</property>+                            <property name="climb_rate">2</property>+                            <property name="digits">2</property>+                            <property name="numeric">True</property>+                            <property name="update_policy">if-valid</property>+                          </object>+                          <packing>+                            <property name="expand">False</property>+                            <property name="fill">True</property>+                            <property name="position">0</property>+                          </packing>+                        </child>+                        <child>+                          <object class="GtkSpinButton" id="bottom-crop-spin-button">+                            <property name="visible">True</property>+                            <property name="can_focus">True</property>+                            <property name="tooltip_text" translatable="yes">How much to crop from the bottom?</property>+                            <property name="primary_icon_stock">gtk-goto-top</property>+                            <property name="primary_icon_activatable">False</property>+                            <property name="secondary_icon_activatable">False</property>+                            <property name="secondary_icon_sensitive">False</property>+                            <property name="input_purpose">digits</property>+                            <property name="adjustment">bottom-crop-adjustment</property>+                            <property name="climb_rate">2</property>+                            <property name="digits">2</property>+                            <property name="numeric">True</property>+                            <property name="update_policy">if-valid</property>+                          </object>+                          <packing>+                            <property name="expand">False</property>+                            <property name="fill">True</property>+                            <property name="position">1</property>+                          </packing>+                        </child>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">1</property>+                      </packing>+                    </child>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">4</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkSeparator" id="crop-top-bottom-text-separator">+                    <property name="visible">True</property>+                    <property name="can_focus">False</property>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">6</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkToggleButton" id="top-bottom-text-toggle-button">+                    <property name="label" translatable="yes"> Top &amp; Bottom Text</property>+                    <property name="name">top-bottom-text-toggle-button</property>+                    <property name="visible">True</property>+                    <property name="can_focus">True</property>+                    <property name="focus_on_click">False</property>+                    <property name="receives_default">True</property>+                    <property name="image">top-bottom-text-toggle-button-image</property>+                    <property name="always_show_image">True</property>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">7</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkBox" id="top-bottom-text-font-chooser-box">+                    <property name="can_focus">False</property>+                    <property name="orientation">vertical</property>+                    <child>+                      <object class="GtkEntry" id="top-text-entry">+                        <property name="visible">True</property>+                        <property name="can_focus">True</property>+                        <property name="tooltip_text" translatable="yes">What text should go on top?</property>+                        <property name="primary_icon_stock">gtk-italic</property>+                        <property name="primary_icon_activatable">False</property>+                        <property name="secondary_icon_activatable">False</property>+                        <property name="secondary_icon_sensitive">False</property>+                        <property name="placeholder_text" translatable="yes">Top Text (optional)</property>+                      </object>+                      <packing>+                        <property name="expand">True</property>+                        <property name="fill">True</property>+                        <property name="position">0</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkEntry" id="bottom-text-entry">+                        <property name="visible">True</property>+                        <property name="can_focus">True</property>+                        <property name="tooltip_text" translatable="yes">What text should go on the bottom?</property>+                        <property name="primary_icon_stock">gtk-italic</property>+                        <property name="primary_icon_activatable">False</property>+                        <property name="secondary_icon_activatable">False</property>+                        <property name="secondary_icon_sensitive">False</property>+                        <property name="placeholder_text" translatable="yes">Bottom Text (optional)</property>+                      </object>+                      <packing>+                        <property name="expand">True</property>+                        <property name="fill">True</property>+                        <property name="position">1</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkFontButton" id="font-chooser-button">+                        <property name="name">font_chooser_button</property>+                        <property name="visible">True</property>+                        <property name="can_focus">True</property>+                        <property name="receives_default">True</property>+                        <property name="tooltip_text" translatable="yes">What font would you like?</property>+                        <property name="font">Sans 12</property>+                        <property name="preview_text">I love making GIFs with Gifcurry!</property>+                        <property name="title" translatable="yes">Gifcurry - Choose a Font</property>+                        <property name="use_font">True</property>+                        <property name="show_style">False</property>+                        <property name="show_size">False</property>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">2</property>+                      </packing>+                    </child>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">8</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkSeparator" id="top-bottom-text-save-open-separator">+                    <property name="visible">True</property>+                    <property name="can_focus">False</property>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">9</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkToggleButton" id="save-open-toggle-button">+                    <property name="label" translatable="yes"> Save &amp; Open</property>+                    <property name="name">save-open-toggle-button</property>+                    <property name="visible">True</property>+                    <property name="can_focus">True</property>+                    <property name="focus_on_click">False</property>+                    <property name="receives_default">True</property>+                    <property name="image">save-toggle-button-image</property>+                    <property name="always_show_image">True</property>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">10</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkBox" id="save-open-box">+                    <property name="can_focus">False</property>+                    <property name="orientation">vertical</property>+                    <child>+                      <object class="GtkBox" id="save-as-gif-or-video-box">+                        <property name="visible">True</property>+                        <property name="can_focus">False</property>+                        <property name="homogeneous">True</property>+                        <child>+                          <object class="GtkRadioButton" id="save-as-gif-radio-button">+                            <property name="label" translatable="yes"> Save as GIF</property>+                            <property name="visible">True</property>+                            <property name="can_focus">True</property>+                            <property name="receives_default">False</property>+                            <property name="tooltip_text" translatable="yes">Save the file as a GIF?</property>+                            <property name="image">gif-icon-image</property>+                            <property name="always_show_image">True</property>+                            <property name="draw_indicator">False</property>+                            <property name="group">save-as-video-radio-button</property>+                          </object>+                          <packing>+                            <property name="expand">False</property>+                            <property name="fill">True</property>+                            <property name="position">0</property>+                          </packing>+                        </child>+                        <child>+                          <object class="GtkRadioButton" id="save-as-video-radio-button">+                            <property name="label" translatable="yes"> Save as Video</property>+                            <property name="visible">True</property>+                            <property name="can_focus">True</property>+                            <property name="receives_default">False</property>+                            <property name="tooltip_text" translatable="yes">Save the file as a video?</property>+                            <property name="image">video-icon-image</property>+                            <property name="always_show_image">True</property>+                            <property name="draw_indicator">False</property>+                            <property name="group">save-as-gif-radio-button</property>+                          </object>+                          <packing>+                            <property name="expand">False</property>+                            <property name="fill">True</property>+                            <property name="pack_type">end</property>+                            <property name="position">1</property>+                          </packing>+                        </child>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">0</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkFileChooserButton" id="out-file-chooser-button">+                        <property name="visible">True</property>+                        <property name="can_focus">True</property>+                        <property name="tooltip_text" translatable="yes">Which folder do you want to save to?</property>+                        <property name="action">select-folder</property>+                        <property name="title" translatable="yes">Gifcurry - Pick a Folder</property>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">1</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkEntry" id="out-file-name-entry">+                        <property name="name">out-file-name-entry</property>+                        <property name="visible">True</property>+                        <property name="can_focus">True</property>+                        <property name="tooltip_text" translatable="yes">What is the file name?</property>+                        <property name="primary_icon_stock">gtk-file</property>+                        <property name="primary_icon_activatable">False</property>+                        <property name="placeholder_text" translatable="yes">Output File Name</property>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">2</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkBox" id="spinner-save-open-box">+                        <property name="visible">True</property>+                        <property name="can_focus">False</property>+                        <property name="homogeneous">True</property>+                        <child>+                          <object class="GtkSpinner" id="save-spinner">+                            <property name="can_focus">False</property>+                          </object>+                          <packing>+                            <property name="expand">True</property>+                            <property name="fill">True</property>+                            <property name="position">0</property>+                          </packing>+                        </child>+                        <child>+                          <object class="GtkButton" id="save-button">+                            <property name="label"> Save</property>+                            <property name="visible">True</property>+                            <property name="can_focus">True</property>+                            <property name="receives_default">True</property>+                            <property name="tooltip_text" translatable="yes">Click here to save the file.</property>+                            <property name="image">save-icon</property>+                            <property name="always_show_image">True</property>+                          </object>+                          <packing>+                            <property name="expand">True</property>+                            <property name="fill">True</property>+                            <property name="position">1</property>+                          </packing>+                        </child>+                        <child>+                          <object class="GtkButton" id="open-button">+                            <property name="label" translatable="yes"> Open</property>+                            <property name="visible">True</property>+                            <property name="can_focus">True</property>+                            <property name="receives_default">True</property>+                            <property name="tooltip_text" translatable="yes">Click here to open the file.</property>+                            <property name="image">open-icon</property>+                            <property name="always_show_image">True</property>+                          </object>+                          <packing>+                            <property name="expand">True</property>+                            <property name="fill">True</property>+                            <property name="pack_type">end</property>+                            <property name="position">2</property>+                          </packing>+                        </child>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">3</property>+                      </packing>+                    </child>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">11</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkSeparator" id="save-open-upload-separator">+                    <property name="visible">True</property>+                    <property name="can_focus">False</property>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">12</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkToggleButton" id="upload-toggle-button">+                    <property name="label" translatable="yes"> Upload</property>+                    <property name="name">upload-toggle-button</property>+                    <property name="visible">True</property>+                    <property name="can_focus">True</property>+                    <property name="focus_on_click">False</property>+                    <property name="receives_default">True</property>+                    <property name="image">upload-toggle-button-image</property>+                    <property name="always_show_image">True</property>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">13</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkBox" id="upload-box">+                    <property name="can_focus">False</property>+                    <child>+                      <object class="GtkButton" id="giphy-upload-button">+                        <property name="label" translatable="yes">Giphy</property>+                        <property name="name">giphy-upload-button</property>+                        <property name="visible">True</property>+                        <property name="can_focus">True</property>+                        <property name="receives_default">True</property>+                        <property name="tooltip_text" translatable="yes">Click here to upload to Giphy.</property>+                        <property name="hexpand">True</property>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">0</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkSeparator" id="giphy-imgur-upload-separator">+                        <property name="visible">True</property>+                        <property name="sensitive">False</property>+                        <property name="can_focus">False</property>+                        <property name="double_buffered">False</property>+                        <property name="valign">baseline</property>+                        <property name="orientation">vertical</property>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">1</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkButton" id="imgur-upload-button">+                        <property name="label" translatable="yes">Imgur</property>+                        <property name="name">imgur-upload-button</property>+                        <property name="visible">True</property>+                        <property name="can_focus">True</property>+                        <property name="receives_default">True</property>+                        <property name="tooltip_text" translatable="yes">Click here to upload to Imgur.</property>+                        <property name="hexpand">True</property>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="pack_type">end</property>+                        <property name="position">2</property>+                      </packing>+                    </child>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="position">14</property>+                  </packing>+                </child>+              </object>+              <packing>+                <property name="expand">False</property>+                <property name="fill">True</property>+                <property name="position">0</property>+              </packing>+            </child>+            <child>+              <object class="GtkSeparator" id="sidebar-preview-separator">+                <property name="visible">True</property>+                <property name="can_focus">False</property>+                <property name="orientation">vertical</property>+              </object>+              <packing>+                <property name="expand">False</property>+                <property name="fill">True</property>+                <property name="position">1</property>+              </packing>+            </child>+            <child>+              <object class="GtkBox" id="preview-start-duration-time-box">+                <property name="visible">True</property>+                <property name="can_focus">False</property>+                <property name="hexpand">True</property>+                <property name="vexpand">True</property>+                <property name="orientation">vertical</property>+                <child>+                  <object class="GtkBox" id="main-preview-box">+                    <property name="width_request">600</property>+                    <property name="visible">True</property>+                    <property name="app_paintable">True</property>+                    <property name="can_focus">False</property>+                    <property name="hexpand">True</property>+                    <property name="vexpand">True</property>+                    <property name="orientation">vertical</property>+                    <child>+                      <placeholder/>+                    </child>+                  </object>+                  <packing>+                    <property name="expand">True</property>+                    <property name="fill">True</property>+                    <property name="position">0</property>+                  </packing>+                </child>+                <child>+                  <object class="GtkBox" id="start-duration-time-video-duration-box">+                    <property name="visible">True</property>+                    <property name="can_focus">False</property>+                    <property name="hexpand">True</property>+                    <property name="orientation">vertical</property>+                    <child>+                      <object class="GtkBox" id="start-duration-time-box">+                        <property name="visible">True</property>+                        <property name="can_focus">False</property>+                        <property name="homogeneous">True</property>+                        <child>+                          <object class="GtkSpinButton" id="start-time-spin-button">+                            <property name="name">start-time-spin-button</property>+                            <property name="width_request">150</property>+                            <property name="visible">True</property>+                            <property name="can_focus">True</property>+                            <property name="tooltip_text" translatable="yes">What is the start time?</property>+                            <property name="hexpand">True</property>+                            <property name="text" translatable="yes">0.00</property>+                            <property name="truncate_multiline">True</property>+                            <property name="primary_icon_stock">gtk-indent</property>+                            <property name="primary_icon_activatable">False</property>+                            <property name="secondary_icon_activatable">False</property>+                            <property name="secondary_icon_sensitive">False</property>+                            <property name="placeholder_text" translatable="yes">Start Time (seconds)</property>+                            <property name="input_purpose">digits</property>+                            <property name="adjustment">start-time-adjustment</property>+                            <property name="climb_rate">1</property>+                            <property name="digits">2</property>+                            <property name="numeric">True</property>+                            <property name="update_policy">if-valid</property>+                          </object>+                          <packing>+                            <property name="expand">False</property>+                            <property name="fill">True</property>+                            <property name="position">0</property>+                          </packing>+                        </child>+                        <child>+                          <object class="GtkSpinButton" id="duration-time-spin-button">+                            <property name="name">duration-time-spin-button</property>+                            <property name="width_request">150</property>+                            <property name="visible">True</property>+                            <property name="can_focus">True</property>+                            <property name="tooltip_text" translatable="yes">How long does the GIF run?</property>+                            <property name="hexpand">True</property>+                            <property name="text" translatable="yes">0.00</property>+                            <property name="primary_icon_stock">gtk-refresh</property>+                            <property name="primary_icon_activatable">False</property>+                            <property name="secondary_icon_activatable">False</property>+                            <property name="secondary_icon_sensitive">False</property>+                            <property name="placeholder_text" translatable="yes">Duration Time (seconds)</property>+                            <property name="input_purpose">digits</property>+                            <property name="adjustment">duration-time-adjustment</property>+                            <property name="climb_rate">1</property>+                            <property name="digits">2</property>+                            <property name="numeric">True</property>+                            <property name="update_policy">if-valid</property>+                          </object>+                          <packing>+                            <property name="expand">False</property>+                            <property name="fill">True</property>+                            <property name="pack_type">end</property>+                            <property name="position">1</property>+                          </packing>+                        </child>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">0</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkLevelBar" id="start-time-level-bar">+                        <property name="visible">True</property>+                        <property name="can_focus">False</property>+                        <property name="hexpand">True</property>+                        <property name="value">0.029999999999999999</property>+                        <property name="max_value">0.029999999999999999</property>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="position">3</property>+                      </packing>+                    </child>+                    <child>+                      <object class="GtkLevelBar" id="end-time-level-bar">+                        <property name="visible">True</property>+                        <property name="can_focus">False</property>+                        <property name="hexpand">True</property>+                        <property name="value">0.01</property>+                        <property name="max_value">0.01</property>+                        <property name="inverted">True</property>+                      </object>+                      <packing>+                        <property name="expand">False</property>+                        <property name="fill">True</property>+                        <property name="pack_type">end</property>+                        <property name="position">4</property>+                      </packing>+                    </child>+                  </object>+                  <packing>+                    <property name="expand">False</property>+                    <property name="fill">True</property>+                    <property name="pack_type">end</property>+                    <property name="position">1</property>+                  </packing>+                </child>+              </object>+              <packing>+                <property name="expand">True</property>+                <property name="fill">True</property>+                <property name="pack_type">end</property>+                <property name="position">2</property>+              </packing>+            </child>+          </object>+          <packing>+            <property name="expand">True</property>+            <property name="fill">True</property>+            <property name="position">2</property>+          </packing>+        </child>+        <child>+          <object class="GtkBox" id="status-box">+            <property name="visible">True</property>+            <property name="sensitive">False</property>+            <property name="can_focus">False</property>+            <property name="hexpand">True</property>+            <property name="baseline_position">top</property>+            <child>+              <object class="GtkEntry" id="status-entry">+                <property name="visible">True</property>+                <property name="sensitive">False</property>+                <property name="app_paintable">True</property>+                <property name="can_focus">True</property>+                <property name="hexpand">True</property>+                <property name="editable">False</property>+                <property name="has_frame">False</property>+                <property name="text" translatable="yes">Ready.</property>+                <property name="shadow_type">none</property>+                <property name="caps_lock_warning">False</property>+                <property name="primary_icon_stock">gtk-info</property>+                <property name="primary_icon_activatable">False</property>+                <property name="secondary_icon_activatable">False</property>+                <property name="primary_icon_sensitive">False</property>+                <property name="secondary_icon_sensitive">False</property>+              </object>+              <packing>+                <property name="expand">False</property>+                <property name="fill">True</property>+                <property name="position">0</property>+              </packing>+            </child>+          </object>+          <packing>+            <property name="expand">False</property>+            <property name="fill">True</property>+            <property name="pack_type">end</property>+            <property name="position">3</property>+          </packing>+        </child>+      </object>+    </child>+    <child type="titlebar">+      <placeholder/>+    </child>+  </object>+  <object class="GtkAboutDialog" id="about-dialog">+    <property name="can_focus">False</property>+    <property name="title" translatable="yes">Gifcurry - About</property>+    <property name="window_position">center</property>+    <property name="destroy_with_parent">True</property>+    <property name="icon_name">dialog-information</property>+    <property name="type_hint">dialog</property>+    <property name="skip_taskbar_hint">True</property>+    <property name="gravity">center</property>+    <property name="transient_for">gifcurry-window</property>+    <property name="attached_to">gifcurry-window</property>+    <property name="program_name">Gifcurry</property>+    <property name="version">3.0.0.0</property>     <property name="copyright" translatable="yes">(C) 2016 David Lettier lettier.com</property>     <property name="website">https://github.com/lettier/gifcurry</property>
+ src/gui/GuiMisc.hs view
@@ -0,0 +1,66 @@+{-+  Gifcurry+  (C) 2018 David Lettier+  lettier.com+-}++{-# LANGUAGE OverloadedStrings #-}++module GuiMisc where++import System.Directory+import Text.Read+import Data.Int+import Data.Maybe+import Data.Text+import qualified GI.Gtk++enumToInt32 :: (Enum a, Ord a) => a -> Int32+enumToInt32 enum = fromIntegral (fromEnum enum) :: Int32++floatToInt32 :: Float -> Int32+floatToInt32 f = enumToInt32 (round f :: Int)++int32ToDouble :: Int32 -> Double+int32ToDouble = fromIntegral++int32ToFloat :: Int32 -> Float+int32ToFloat = fromIntegral++entryGetMaybeFloat :: GI.Gtk.Entry -> IO (Maybe Float)+entryGetMaybeFloat entry = do+  text <- Data.Text.strip <$> GI.Gtk.entryGetText entry+  let containsPeriod = isJust $ Data.Text.find (== '.') text+  let string = Data.Text.unpack text+  let maybeFloat = readMaybe (string ++ if containsPeriod then "0" else "") :: Maybe Float+  return maybeFloat++entryGetFloat :: GI.Gtk.Entry -> Float -> IO Float+entryGetFloat entry nothing = do+  maybeFloat <- entryGetMaybeFloat entry+  return $ fromMaybe nothing maybeFloat++entryGetMaybeInt :: GI.Gtk.Entry -> IO (Maybe Int)+entryGetMaybeInt entry = do+  text <- Data.Text.strip <$> GI.Gtk.entryGetText entry+  let string = Data.Text.unpack text+  let maybeInt = readMaybe string :: Maybe Int+  return maybeInt++fileChooserButtonGetString :: GI.Gtk.FileChooserButton -> IO String+fileChooserButtonGetString =+  fmap+      (Data.Text.unpack . Data.Text.strip . Data.Text.pack . fromMaybe "")+    . GI.Gtk.fileChooserGetFilename++fileChooserButtonGetFilePath :: GI.Gtk.FileChooserButton -> IO String+fileChooserButtonGetFilePath fileChooserButton = do+  result <- fileChooserButtonGetString fileChooserButton+  fileExist <- doesFileExist result+  return $+    if fileExist+      then result+      else ""++safeDivide :: (Fractional a, Eq a) => a -> a -> Maybe a+safeDivide n d = if d == 0.0 then Nothing else Just $ n / d
+ src/gui/GuiPreview.hs view
@@ -0,0 +1,591 @@+{-+  Gifcurry+  (C) 2018 David Lettier+  lettier.com+-}++{-# LANGUAGE+    OverloadedStrings+  , NamedFieldPuns+#-}++module GuiPreview where++import GHC.Float+import System.FilePath+import System.IO.Temp+import Data.Int+import Data.Maybe+import Data.Text+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.Gtk+import qualified GI.Gst+import qualified GI.Cairo+import qualified GiCairoCairoBridge+import qualified Graphics.Rendering.Cairo as GRC++import qualified Gifcurry+  ( gif+  , GifParams(..)+  , defaultGifParams+  )+import qualified GtkMainSyncAsync (gtkMainSync, gtkMainAsync)+import qualified GuiRecords as GR+import GuiMisc++blankPreviewIcon :: String+blankPreviewIcon = "gtk-discard"++framePreviewDirectoryName :: String+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)++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++runPreviewLoopIfNotRunning :: GR.GuiComponents -> IO Bool -> IO ()+runPreviewLoopIfNotRunning+  GR.GuiComponents+    { GR.guiPreviewStateRef }+  f+  = do+  loopRunning <- getLoopRunning+  unless loopRunning $+    void $+      GI.GLib.timeoutAdd+        GI.GLib.PRIORITY_DEFAULT+        1+        f+  where+    getLoopRunning :: IO Bool+    getLoopRunning =+      readIORef guiPreviewStateRef >>=+        \ x -> return $ GR.loopRunning x++preview+  ::  GR.GuiComponents+  ->  (  GR.GuiComponents+      -> GR.GuiPreviewState+      -> String+      -> Float+      -> Float+      -> Float+      -> Float+      -> IO ()+      )+  ->  IO Bool+preview+  guiComponents@GR.GuiComponents+    { GR.inFileChooserButton+    , GR.startTimeSpinButton+    , GR.durationTimeSpinButton+    , GR.guiPreviewStateRef+    , GR.inVideoPropertiesRef+    }+  f+  = do+  GR.InVideoProperties+    { GR.inVideoWidth+    , GR.inVideoHeight+    } <- readIORef inVideoPropertiesRef+  guiPreviewState <- readIORef guiPreviewStateRef+  inFilePath      <- fileChooserButtonGetFilePath inFileChooserButton+  startTime       <- double2Float <$> GI.Gtk.spinButtonGetValue startTimeSpinButton+  durationTime    <- double2Float <$> GI.Gtk.spinButtonGetValue durationTimeSpinButton+  let invalidInFilePath    = inFilePath    == ""+  let invalidStartTime     = startTime     < 0.0+  let invalidDurationTime  = durationTime  <= 0.0+  let invalidInVideoWidth  = inVideoWidth  <= 0.0+  let invalidInVideoHeight = inVideoHeight <= 0.0+  let inputInvalid =+           invalidInFilePath+        || invalidStartTime+        || invalidDurationTime+        || invalidInVideoWidth+        || invalidInVideoHeight+  if not inputInvalid+    then+      f+        guiComponents+        guiPreviewState+        inFilePath+        startTime+        durationTime+        inVideoWidth+        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+      }+  return True++videoPreview+  ::  GR.GuiComponents+  ->  GR.GuiPreviewState+  ->  String+  ->  Float+  ->  Float+  ->  Float+  ->  Float+  ->  IO ()+videoPreview+  GR.GuiComponents+    { GR.window+    , GR.cropToggleButton+    , GR.mainPreviewBox+    , GR.videoPreviewBox+    , GR.maybePlaybinElement = (Just playbinElement)+    }+  GR.GuiPreviewState+    { GR.maybeInFilePath+    , GR.maybeStartTime+    }+  inFilePath+  startTime+  durationTime+  inVideoWidth+  inVideoHeight+  = do+  GI.Gtk.widgetShow videoPreviewBox+  sizePreviewAndWindow+  handleChanges+  handleCropMode+  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+      GI.Gtk.widgetSetSizeRequest+        window+        previewWidth'+        (-1)+      GI.Gtk.widgetSetSizeRequest+        mainPreviewBox+        previewWidth'+        previewHeight'+      resizeWindow window+    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+      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 seekToStart = startTimeChanged || inFilePathChanged || startOver+      when inFilePathChanged $ do+        _ <- GI.Gst.elementSetState playbinElement GI.Gst.StateNull+        Data.GI.Base.Properties.setObjectPropertyString+          playbinElement+          "uri"+          (Just $ pack $ "file://" ++ inFilePath)+        _ <- Data.GI.Base.Properties.setObjectPropertyDouble playbinElement "volume" 0.0+        resizeWindow window+      when seekToStart $ do+        _ <- GI.Gst.elementSetState playbinElement GI.Gst.StatePaused+        _ <- GI.Gst.elementSeekSimple+          playbinElement+          GI.Gst.FormatTime+          [GI.Gst.SeekFlagsFlush]+          startTimeInNano+        _ <- GI.Gst.elementSetState playbinElement GI.Gst.StatePlaying+        return ()+    handleCropMode :: IO ()+    handleCropMode = do+      cropModeEnabled <- GI.Gtk.getToggleButtonActive cropToggleButton+      if cropModeEnabled+        then+        void $ Data.GI.Base.Properties.setObjectPropertyBool+          playbinElement+          "force-aspect-ratio"+          False+        else+          void $ Data.GI.Base.Properties.setObjectPropertyBool+            playbinElement+            "force-aspect-ratio"+            True+videoPreview _ _ _ _ _ _ _ = return ()++firstAndLastFramePreview+  ::  GR.GuiComponents+  ->  GR.GuiPreviewState+  ->  String+  ->  Float+  ->  Float+  ->  Float+  ->  Float+  ->  IO ()+firstAndLastFramePreview+  GR.GuiComponents+    { GR.window+    , GR.imagesPreviewBox+    , GR.firstFrameImage+    , GR.lastFrameImage+    , GR.firstFramePreviewImageDrawingArea+    , GR.lastFramePreviewImageDrawingArea+    , GR.temporaryDirectory+    }+  GR.GuiPreviewState+    { GR.maybeInFilePath+    , GR.maybeStartTime+    , GR.maybeDurationTime+    }+  inFilePath+  startTime+  durationTime+  _+  _+  = do+  GI.Gtk.widgetShow imagesPreviewBox+  handleChanges+  redrawDrawingAreas+  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 firstAndLastFrameDirty = inFilePathChanged || startTimeChanged+      let lastFrameDirty = not firstAndLastFrameDirty && durationTimeChanged+      when firstAndLastFrameDirty $+        makeFirstAndLastFramePreview+          inFilePath+          startTime+          durationTime+          firstFrameImage+          lastFrameImage+          temporaryDirectory+          window+      when lastFrameDirty $+        makeLastFramePreview+          inFilePath+          startTime+          durationTime+          lastFrameImage+          temporaryDirectory+          window+    redrawDrawingAreas :: IO ()+    redrawDrawingAreas = do+      GI.Gtk.widgetQueueDraw firstFramePreviewImageDrawingArea+      GI.Gtk.widgetQueueDraw lastFramePreviewImageDrawingArea++drawCropGrid :: GR.GuiComponents -> GI.Gtk.DrawingArea -> GI.Cairo.Context -> IO Bool+drawCropGrid+  GR.GuiComponents+    { GR.cropToggleButton+    , GR.leftCropSpinButton+    , GR.rightCropSpinButton+    , GR.topCropSpinButton+    , GR.bottomCropSpinButton+    , GR.inVideoPropertiesRef+    }+  drawingArea+  context+  = do+  GR.InVideoProperties+    { GR.inVideoWidth+    , GR.inVideoHeight+    } <- 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) $+    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+  return False++resetWindow :: GR.GuiComponents -> IO ()+resetWindow+  GR.GuiComponents+    { GR.window+    , GR.mainPreviewBox+    , GR.imagesPreviewBox+    , GR.videoPreviewBox+    }+  = do+  GI.Gtk.widgetSetSizeRequest+    window+    900+    (-1)+  GI.Gtk.widgetSetSizeRequest+    mainPreviewBox+    (floatToInt32 desiredPreviewSize)+    (-1)+  GI.Gtk.widgetHide imagesPreviewBox+  GI.Gtk.widgetHide videoPreviewBox+  resizeWindow window++resizeWindow :: GI.Gtk.Window -> IO ()+resizeWindow window =+  void $ GI.Gtk.windowResize window 1 1++makeFirstAndLastFramePreview+  ::  String+  ->  Float+  ->  Float+  ->  GI.Gtk.Image+  ->  GI.Gtk.Image+  ->  System.FilePath.FilePath+  ->  GI.Gtk.Window+  ->  IO ()+makeFirstAndLastFramePreview+  inFilePath+  startTime+  durationTime+  firstFrameImage+  lastFrameImage+  temporaryDirectory+  window+  = do+  void $ forkIO $+    withTempDirectory+      temporaryDirectory+      framePreviewDirectoryName $+        \ tmpDir -> do+          let outFilePath = tmpDir ++ "/gifcurry-first-frame-preview.gif"+          let inputValid  = not (Data.List.null inFilePath) && startTime >= 0.0+          void $+            setOrResetFramePrevew+              inputValid+              inFilePath+              outFilePath+              startTime+              firstFrameImage+              ""+              window+  makeLastFramePreview+    inFilePath+    startTime+    durationTime+    lastFrameImage+    temporaryDirectory+    window++makeLastFramePreview+  ::  String+  ->  Float+  ->  Float+  ->  GI.Gtk.Image+  ->  System.FilePath.FilePath+  ->  GI.Gtk.Window+  ->  IO ()+makeLastFramePreview+  inFilePath+  startTime+  durationTime+  lastFrameImage+  temporaryDirectory+  window+  =+  void $ forkIO $+    withTempDirectory+      temporaryDirectory+      framePreviewDirectoryName $+        \ tmpdir -> do+          let startTime'  = startTime + durationTime+          let outFilePath = tmpdir ++ "/gifcurry-last-frame-preview.gif"+          let inputValid  = startTime' > 0.0 && not (Data.List.null inFilePath)+          void $+            setOrResetFramePrevew+              inputValid+              inFilePath+              outFilePath+              startTime'+              lastFrameImage+              ""+              window++setOrResetFramePrevew+  ::  Bool+  ->  String+  ->  String+  ->  Float+  ->  GI.Gtk.Image+  ->  String+  ->  GI.Gtk.Window+  ->  IO ()+setOrResetFramePrevew False _ _ _ image _ window =+  GtkMainSyncAsync.gtkMainAsync $ do+    resetImage image+    resizeWindow window+setOrResetFramePrevew+  True+  inFilePath+  outFilePath+  time+  image+  overlay+  window+  = do+  result <- makeImagePreview inFilePath outFilePath time overlay+  case result of+    Left _ ->+      void $ updatePreviewFrame "" image False+    Right filePath ->+      void $ updatePreviewFrame filePath image True+  GtkMainSyncAsync.gtkMainAsync $ resizeWindow window++makeImagePreview+  ::  String+  ->  String+  ->  Float+  ->  String+  ->  IO (Either IOError String)+makeImagePreview inputFile outputFile startTime bottomText =+  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+      }++updatePreviewFrame :: String -> GI.Gtk.Image -> Bool -> IO ()+updatePreviewFrame filePath image True =+  GtkMainSyncAsync.gtkMainSync (GI.Gtk.imageSetFromFile image (Just filePath))+updatePreviewFrame _ image False =+  GtkMainSyncAsync.gtkMainSync (resetImage image)++resetImage :: GI.Gtk.Image -> IO ()+resetImage image =+  GI.Gtk.imageSetFromIconName+    image+    (Just $ pack blankPreviewIcon)+    (enumToInt32 GI.Gtk.IconSizeButton)++secondsToNanoseconds :: Float -> Int64+secondsToNanoseconds s =+  fromIntegral (round (s * 1000000000.0) :: Integer) :: Int64++desiredPreviewSize :: Float+desiredPreviewSize = 600.0
+ src/gui/GuiRecords.hs view
@@ -0,0 +1,104 @@+{-+  Gifcurry+  (C) 2018 David Lettier+  lettier.com+-}++module GuiRecords where++import Data.IORef+import qualified GI.Gtk+import GI.Gst++data GuiComponents =+  GuiComponents+    { window                            :: GI.Gtk.Window+    , 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+    , bottomCropSpinButton              :: GI.Gtk.SpinButton+    , inFileChooserButton               :: GI.Gtk.FileChooserButton+    , outFileChooserButton              :: GI.Gtk.FileChooserButton+    , fontChooserButton                 :: GI.Gtk.FontButton+    , saveButton                        :: GI.Gtk.Button+    , openButton                        :: GI.Gtk.Button+    , yesGtkButton                      :: GI.Gtk.Button+    , noGtkButton                       :: GI.Gtk.Button+    , aboutButton                       :: GI.Gtk.Button+    , giphyUploadButton                 :: GI.Gtk.Button+    , imgurUploadButton                 :: GI.Gtk.Button+    , saveAsVideoRadioButton            :: GI.Gtk.RadioButton+    , widthQualityPercentToggleButton   :: GI.Gtk.ToggleButton+    , cropToggleButton                  :: GI.Gtk.ToggleButton+    , topBottomTextToggleButton         :: GI.Gtk.ToggleButton+    , saveOpenToggleButton              :: GI.Gtk.ToggleButton+    , uploadToggleButton                :: GI.Gtk.ToggleButton+    , 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+    , mainPreviewBox                    :: GI.Gtk.Box+    , imagesPreviewBox                  :: GI.Gtk.Box+    , videoPreviewBox                   :: GI.Gtk.Box+    , videoPreviewOverlayChildBox       :: GI.Gtk.Box+    , widthQualityPercentBox            :: GI.Gtk.Box+    , cropSpinButtonsBox                :: GI.Gtk.Box+    , topBottomTextFontChooserBox       :: GI.Gtk.Box+    , saveOpenBox                       :: GI.Gtk.Box+    , uploadBox                         :: GI.Gtk.Box+    , videoPreviewDrawingArea           :: GI.Gtk.DrawingArea+    , firstFramePreviewImageDrawingArea :: GI.Gtk.DrawingArea+    , lastFramePreviewImageDrawingArea  :: GI.Gtk.DrawingArea+    , firstFrameImage                   :: GI.Gtk.Image+    , lastFrameImage                    :: GI.Gtk.Image+    , longGifGtkMessageDialog           :: GI.Gtk.MessageDialog+    , aboutDialog                       :: GI.Gtk.AboutDialog+    , startTimeLevelBar                 :: GI.Gtk.LevelBar+    , endTimeLevelBar                   :: GI.Gtk.LevelBar+    , saveSpinner                       :: GI.Gtk.Spinner+    , maybeVideoPreviewWidget           :: Maybe GI.Gtk.Widget+    , maybePlaybinElement               :: Maybe GI.Gst.Element+    , temporaryDirectory                :: FilePath+    , guiPreviewStateRef                :: IORef GuiPreviewState+    , inVideoPropertiesRef              :: IORef InVideoProperties+    }++data GuiPreviewState =+  GuiPreviewState+    { maybeInFilePath   :: Maybe String+    , maybeStartTime    :: Maybe Float+    , maybeDurationTime :: Maybe Float+    , loopRunning       :: Bool+    }++data InVideoProperties =+  InVideoProperties+    { inVideoDuration :: Float+    , inVideoWidth    :: Float+    , inVideoHeight   :: Float+    }++defaultGuiPreviewState :: GuiPreviewState+defaultGuiPreviewState =+  GuiPreviewState+    { maybeInFilePath = Nothing+    , maybeStartTime = Nothing+    , maybeDurationTime = Nothing+    , loopRunning = False+    }++defaultInVideoProperties :: InVideoProperties+defaultInVideoProperties =+  InVideoProperties+    { inVideoDuration = 0.0+    , inVideoWidth = 0.0+    , inVideoHeight = 0.0+    }
src/gui/Main.hs view
@@ -4,336 +4,870 @@   lettier.com -} -{-# LANGUAGE OverloadedStrings #-}--import System.Directory-import System.Process-import System.FilePath-import System.Info-import System.IO.Temp-import Text.Read-import Data.Int-import Data.Maybe-import Data.Either-import Data.Char-import Data.Text-import Data.List-import Control.Monad-import Control.Concurrent-import Data.GI.Base-import GI.GObject-import qualified GI.Gtk--import Paths_Gifcurry-import qualified Gifcurry (-      gif-    , GifParams(..)-    , defaultGifParams-    , gifParamsValid-    , getVideoDurationInSeconds-    , findOrCreateTemporaryDirectory-  )-import qualified GtkMainSyncAsync (gtkMainSync, gtkMainAsync)--main :: IO ()-main = do-  _ <- GI.Gtk.init Nothing--  builder <- buildBuilder--  window                  <- builderGetObject GI.Gtk.Window            builder "gifcurry-window"-  startTimeEntry          <- builderGetObject GI.Gtk.Entry             builder "start-time-text-entry"-  durationTimeEntry       <- builderGetObject GI.Gtk.Entry             builder "duration-text-entry"-  widthEntry              <- builderGetObject GI.Gtk.Entry             builder "width-text-entry"-  qualityEntry            <- builderGetObject GI.Gtk.Entry             builder "quality-text-entry"-  topTextEntry            <- builderGetObject GI.Gtk.Entry             builder "top-text-text-entry"-  bottomTextEntry         <- builderGetObject GI.Gtk.Entry             builder "bottom-text-text-entry"-  outputFileNameEntry     <- builderGetObject GI.Gtk.Entry             builder "output-file-name-text-entry"-  statusEntry             <- builderGetObject GI.Gtk.Entry             builder "status-text-entry"-  inputFileButton         <- builderGetObject GI.Gtk.FileChooserButton builder "input-file-button"-  outputFilePathButton    <- builderGetObject GI.Gtk.FileChooserButton builder "output-file-path-button"-  fontChooserButton       <- builderGetObject GI.Gtk.FontButton        builder "font-chooser-button"-  createButton            <- builderGetObject GI.Gtk.Button            builder "create-button"-  openButton              <- builderGetObject GI.Gtk.Button            builder "open-button"-  giphyButton             <- builderGetObject GI.Gtk.Button            builder "giphy-link-button"-  imgurButton             <- builderGetObject GI.Gtk.Button            builder "imgur-link-button"-  firstFrameImage         <- builderGetObject GI.Gtk.Image             builder "first-frame-image"-  lastFrameImage          <- builderGetObject GI.Gtk.Image             builder "last-frame-image"-  longGifGtkMessageDialog <- builderGetObject GI.Gtk.MessageDialog     builder "long-gif-message-dialog"-  yesGtkButton            <- builderGetObject GI.Gtk.Button            builder "yes-button"-  noGtkButton             <- builderGetObject GI.Gtk.Button            builder "no-button"-  aboutButton             <- builderGetObject GI.Gtk.Button            builder "about-button"-  aboutDialog             <- builderGetObject GI.Gtk.AboutDialog       builder "about-dialog"--  -- Bug in Glade does not allow setting the link button label.-  GI.Gtk.buttonSetLabel giphyButton "Giphy"-  GI.Gtk.buttonSetLabel imgurButton "Imgur"--  GI.Gtk.entrySetText qualityEntry "100"-  GI.Gtk.entrySetText widthEntry   "500"--  -- Glade does not allow us to use the response ID nicknames so we setup them up programmatically here.-  GI.Gtk.dialogAddActionWidget longGifGtkMessageDialog yesGtkButton (enumToInt32 GI.Gtk.ResponseTypeYes)-  GI.Gtk.dialogAddActionWidget longGifGtkMessageDialog noGtkButton  (enumToInt32 GI.Gtk.ResponseTypeNo)--  temporaryDirectory <- Gifcurry.findOrCreateTemporaryDirectory--  _ <- GI.Gtk.onFileChooserButtonFileSet inputFileButton $ do-    setStatusEntryReady statusEntry-    GI.Gtk.imageSetFromIconName firstFrameImage-      (Just $ pack blankPreviewIcon) (enumToInt32 GI.Gtk.IconSizeButton)-    GI.Gtk.imageSetFromIconName lastFrameImage-      (Just $ pack blankPreviewIcon) (enumToInt32 GI.Gtk.IconSizeButton)-    inputFilePathName <- inputFileButtonGetText inputFileButton-    maybeDuration <- Gifcurry.getVideoDurationInSeconds (-        Gifcurry.defaultGifParams { Gifcurry.inputFile = inputFilePathName }-      )-    let duration = fromMaybe 0.5 maybeDuration - 0.5-    let durationText = if duration == 0.0 then "" else pack (show duration)-    let startText = if duration == 0.0 then "" else "0.0"-    _ <- GI.Gtk.entrySetText startTimeEntry startText-    _ <- GI.Gtk.entrySetText durationTimeEntry durationText-    unless (Data.Text.null startText || Data.Text.null durationText) (-        makeFirstFramePreview-          inputFileButton-          startTimeEntry-          durationTimeEntry-          firstFrameImage-          lastFrameImage-          temporaryDirectory-      )--  _ <- GI.Gtk.onEditableChanged startTimeEntry $-    makeFirstFramePreview-      inputFileButton-      startTimeEntry-      durationTimeEntry-      firstFrameImage-      lastFrameImage-      temporaryDirectory--  _ <- GI.Gtk.onEditableChanged durationTimeEntry $-    makeLastFramePreview-      inputFileButton-      startTimeEntry-      durationTimeEntry-      lastFrameImage-      temporaryDirectory--  _ <- GI.Gtk.onWidgetButtonReleaseEvent createButton $ \ _ -> do-    inputFilePathName <- inputFileButtonGetText inputFileButton-    startTime         <- entryGetFloat startTimeEntry (-1.0)-    durationTime      <- entryGetFloat durationTimeEntry (-1.0)-    widthSize'        <- entryGetFloat widthEntry 0.0-    let widthSize     = truncate widthSize'-    qualityPercent    <- entryGetFloat qualityEntry (-1.0)-    fontChoice        <- GI.Gtk.fontButtonGetFontName fontChooserButton-    topText           <- GI.Gtk.entryGetText topTextEntry-    bottomText        <- GI.Gtk.entryGetText bottomTextEntry-    (_, outputFilePathName) <- assembleOutputFilePathName outputFilePathButton outputFileNameEntry-    let params = Gifcurry.defaultGifParams {-        Gifcurry.inputFile      = inputFilePathName-      , Gifcurry.outputFile     = outputFilePathName-      , Gifcurry.startTime      = startTime-      , Gifcurry.durationTime   = durationTime-      , Gifcurry.widthSize      = widthSize-      , Gifcurry.qualityPercent = qualityPercent-      , Gifcurry.fontChoice     = unpack fontChoice-      , Gifcurry.topText        = unpack topText-      , Gifcurry.bottomText     = unpack bottomText-    }-    paramsValid <- Gifcurry.gifParamsValid params-    setStatusEntryReady statusEntry-    if paramsValid-      then do-        longGifGtkMessageDialogResponse <- if durationTime >= durationTimeWarningLevel-                                              then GI.Gtk.dialogRun longGifGtkMessageDialog-                                              else return (enumToInt32 GI.Gtk.ResponseTypeYes)-        when (longGifGtkMessageDialogResponse == enumToInt32 GI.Gtk.ResponseTypeYes) $-          void $ forkOS $ do-            GtkMainSyncAsync.gtkMainAsync $ GI.Gtk.entrySetText statusEntry "One GIF coming up!"-            success <- (ioSuccess . Gifcurry.gif) params-            if not success-              then GtkMainSyncAsync.gtkMainAsync $ GI.Gtk.entrySetText statusEntry "Did not work. Check your settings."-              else do-                _ <- forkOS $ GtkMainSyncAsync.gtkMainAsync $ openGifCommand outputFilePathName-                GtkMainSyncAsync.gtkMainAsync $ setStatusEntryReady statusEntry-      else GI.Gtk.entrySetText statusEntry "Settings are wrong."-    return True--  _ <- GI.Gtk.onWidgetButtonReleaseEvent openButton $ \ _ -> do-    (_, outputFilePathName) <- assembleOutputFilePathName outputFilePathButton outputFileNameEntry-    fileExists <- doesFileExist outputFilePathName-    if fileExists-      then void $ forkIO $ GtkMainSyncAsync.gtkMainAsync $ openGifCommand outputFilePathName-      else GI.Gtk.entrySetText statusEntry "GIF does not exist. Check your settings."-    return True--  _ <- GI.Gtk.onWidgetButtonReleaseEvent aboutButton $ \ _ -> GI.Gtk.dialogRun aboutDialog >> return True--  _ <- GI.Gtk.onDialogResponse longGifGtkMessageDialog (\ _ -> GI.Gtk.widgetHide longGifGtkMessageDialog)-  _ <- GI.Gtk.onDialogResponse aboutDialog             (\ _ -> GI.Gtk.widgetHide aboutDialog)--  _ <- GI.Gtk.onWidgetDestroy window GI.Gtk.mainQuit--  GI.Gtk.widgetShowAll window-  GI.Gtk.main--buildBuilder :: IO GI.Gtk.Builder-buildBuilder = do-  gladeFile <- getDataFileName "data/gui.glade"-  GI.Gtk.builderNewFromFile (pack gladeFile)--builderGetObject ::-  (GI.GObject.GObject b, GI.Gtk.IsBuilder a) =>-  (Data.GI.Base.ManagedPtr b -> b) ->-  a ->-  Prelude.String ->-  IO b-builderGetObject objectTypeClass builder objectId =-  fromJust <$> GI.Gtk.builderGetObject builder (pack objectId) >>=-  GI.Gtk.unsafeCastTo objectTypeClass--inputFileButtonGetText :: GI.Gtk.FileChooserButton -> IO String-inputFileButtonGetText inputFileButton = do-  inputFilePathName <- fileChooserButtonGetText inputFileButton-  fileExist <- doesFileExist inputFilePathName-  if fileExist then return inputFilePathName else return ""--durationTimeWarningLevel :: Float-durationTimeWarningLevel = 10.0--entryGetFloat :: GI.Gtk.Entry -> Float -> IO Float-entryGetFloat e nothing = fmap (\ t -> fromMaybe nothing (readMaybe (unpack t) :: Maybe Float)) (GI.Gtk.entryGetText e)--assembleOutputFilePathName :: GI.Gtk.FileChooserButton -> GI.Gtk.Entry -> IO (String, String)-assembleOutputFilePathName outputFilePathButton outputFileNameEntry = do-  outputFilePath <- fileChooserButtonGetText outputFilePathButton-  outputFileName <- unpack . Data.Text.strip <$> GI.Gtk.entryGetText outputFileNameEntry-  let outputGifFileName = outputFileName ++ ".gif"-  let outputFilePathName =-        if (not . Data.List.null) outputFilePath-          then outputFilePath ++ [System.FilePath.pathSeparator] ++ outputGifFileName-          else outputGifFileName-  return (outputGifFileName, outputFilePathName)--fileChooserButtonGetText :: GI.Gtk.FileChooserButton -> IO String-fileChooserButtonGetText =-  fmap (Data.Text.unpack . Data.Text.strip . Data.Text.pack . fromMaybe "") . GI.Gtk.fileChooserGetFilename--openGifCommand :: String -> IO ()-openGifCommand outputFilePathName = do-  fileExists <- doesFileExist outputFilePathName-  when fileExists $ void (spawnCommand (command ++ "\"" ++ outputFilePathName ++ "\""))-  where-    command :: String-    command = if "linux" `Data.List.isInfixOf` fmap Data.Char.toLower System.Info.os-                then "xdg-open "-                else "open "--blankPreviewIcon :: String-blankPreviewIcon = "gtk-discard"--resetImage :: GI.Gtk.Image -> IO ()-resetImage image =-  GI.Gtk.imageSetFromIconName-    image-    (Just $ pack blankPreviewIcon)-    (enumToInt32 GI.Gtk.IconSizeButton)--makeGifPreview :: String -> String -> Float -> String -> IO (Either IOError String)-makeGifPreview inputFile outputFile startTime bottomText = Gifcurry.gif $ Gifcurry.defaultGifParams {-      Gifcurry.inputFile      = inputFile-    , Gifcurry.outputFile     = outputFile-    , Gifcurry.startTime      = startTime-    , Gifcurry.durationTime   = 0.001-    , Gifcurry.widthSize      = 200-    , Gifcurry.qualityPercent = 50.0-    , Gifcurry.bottomText     = bottomText-  }--ioSuccess :: IO (Either IOError String) -> IO Bool-ioSuccess = fmap isRight--framePreviewDirectoryName :: String-framePreviewDirectoryName = "gifcurry-frame-previews"--makeLastFramePreview ::-  GI.Gtk.FileChooserButton ->-  GI.Gtk.Entry ->-  GI.Gtk.Entry ->-  GI.Gtk.Image ->-  System.FilePath.FilePath ->-  IO ()-makeLastFramePreview-  inputFileButton-  startTimeEntry-  durationTimeEntry-  lastFrameImage-  temporaryDirectory-  =-  void $ forkIO $-    withTempDirectory temporaryDirectory framePreviewDirectoryName $ \ tmpdir -> do-      inputFilePathName           <- inputFileButtonGetText inputFileButton-      startTimeText               <- GI.Gtk.entryGetText startTimeEntry-      durationTimeText            <- GI.Gtk.entryGetText durationTimeEntry-      startTime'                  <- entryGetFloat startTimeEntry (-1.0)-      durationTime                <- entryGetFloat durationTimeEntry 0.001-      let startTimeTextValid      = (not . Data.List.null) (unpack startTimeText)-      let durationTimeTextValid   = (not . Data.List.null) (unpack durationTimeText)-      let startTime               = startTime' + durationTime-      let outputFilePathName      = tmpdir ++ "/gifcurry-last-frame-preview.gif"-      let doSet                   = startTimeTextValid &&-                                    durationTimeTextValid &&-                                    not (Data.List.null inputFilePathName) &&-                                    (startTime > 0.0)-      _ <- setOrResetFramePrevew doSet inputFilePathName outputFilePathName startTime lastFrameImage " LAST  FRAME "-      return()--makeFirstFramePreview ::-  GI.Gtk.FileChooserButton ->-  GI.Gtk.Entry ->-  GI.Gtk.Entry ->-  GI.Gtk.Image ->-  GI.Gtk.Image ->-  System.FilePath.FilePath ->-  IO ()-makeFirstFramePreview-  inputFileButton-  startTimeEntry-  durationTimeEntry-  firstFrameImage-  lastFrameImage-  temporaryDirectory-  =-  void $ forkIO $ do-    withTempDirectory temporaryDirectory framePreviewDirectoryName $ \ tmpDir -> do-      inputFilePathName       <- inputFileButtonGetText inputFileButton-      startTime               <- entryGetFloat startTimeEntry (-1.0)-      let outputFilePathName  = tmpDir ++ "/gifcurry-first-frame-preview.gif"-      let doSet               = not (Data.List.null inputFilePathName) && (startTime >= 0.0)-      _ <- setOrResetFramePrevew doSet inputFilePathName outputFilePathName startTime firstFrameImage " FIRST FRAME "-      return ()-    makeLastFramePreview-      inputFileButton-      startTimeEntry-      durationTimeEntry-      lastFrameImage-      temporaryDirectory--setOrResetFramePrevew :: Bool -> String -> String -> Float -> GI.Gtk.Image -> String -> IO ()-setOrResetFramePrevew False _ _ _ image _ = GtkMainSyncAsync.gtkMainAsync $ resetImage image-setOrResetFramePrevew True inputFilePathName outputFilePathName time image overlay = do-  success <- ioSuccess (makeGifPreview inputFilePathName outputFilePathName time overlay)-  _ <- updatePreviewFrame outputFilePathName image success-  return()--updatePreviewFrame :: String -> GI.Gtk.Image -> Bool -> IO ()-updatePreviewFrame filePathName image True  = GtkMainSyncAsync.gtkMainSync (GI.Gtk.imageSetFromFile image (Just filePathName))-updatePreviewFrame _            image False = GtkMainSyncAsync.gtkMainSync (resetImage image)--setStatusEntryReady :: GI.Gtk.Entry -> IO ()-setStatusEntryReady = flip GI.Gtk.entrySetText "Ready."--enumToInt32 :: (Enum a, Ord a) => a -> Int32-enumToInt32 enum = fromIntegral (fromEnum enum) :: Int32+{-# LANGUAGE+    OverloadedStrings+  , NamedFieldPuns+#-}++import GHC.Float+import System.Directory+import System.FilePath+import System.Process+import Text.Printf+import Data.Maybe+import Data.Text+import qualified Data.ByteString.Char8+import Data.List+import Data.IORef+import Data.Word+import Control.Monad+import Control.Concurrent+import Control.Exception+import Data.GI.Base+import qualified GI.GLib+import qualified GI.GObject+import qualified GI.Gdk+import qualified GI.Gtk+import GI.Gst++import Paths_Gifcurry+import qualified Gifcurry+  ( gif+  , GifParams(..)+  , defaultGifParams+  , gifParamsValid+  , getVideoDurationInSeconds+  , getOutputFileWithExtension+  , getVideoWidthAndHeight+  , findOrCreateTemporaryDirectory+  )+import qualified GtkMainSyncAsync (gtkMainAsync)+import GuiMisc+import qualified GuiRecords as GR+import qualified GuiPreview++durationTimeWarningLevel :: Float+durationTimeWarningLevel = 10.0++main :: IO ()+main = do+  _ <- GI.Gst.init Nothing+  _ <- GI.Gtk.init Nothing++  builder <- buildBuilder++  window                            <- builderGetObject GI.Gtk.Window            builder "gifcurry-window"+  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"+  bottomCropSpinButton              <- builderGetObject GI.Gtk.SpinButton        builder "bottom-crop-spin-button"+  inFileChooserButton               <- builderGetObject GI.Gtk.FileChooserButton builder "in-file-chooser-button"+  outFileChooserButton              <- builderGetObject GI.Gtk.FileChooserButton builder "out-file-chooser-button"+  fontChooserButton                 <- builderGetObject GI.Gtk.FontButton        builder "font-chooser-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"+  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"+  cropToggleButton                  <- builderGetObject GI.Gtk.ToggleButton      builder "crop-toggle-button"+  topBottomTextToggleButton         <- builderGetObject GI.Gtk.ToggleButton      builder "top-bottom-text-toggle-button"+  saveOpenToggleButton              <- builderGetObject GI.Gtk.ToggleButton      builder "save-open-toggle-button"+  uploadToggleButton                <- builderGetObject GI.Gtk.ToggleButton      builder "upload-toggle-button"+  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"+  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"+  cropSpinButtonsBox                <- builderGetObject GI.Gtk.Box               builder "crop-spin-buttons-box"+  topBottomTextFontChooserBox       <- builderGetObject GI.Gtk.Box               builder "top-bottom-text-font-chooser-box"+  saveOpenBox                       <- builderGetObject GI.Gtk.Box               builder "save-open-box"+  uploadBox                         <- builderGetObject GI.Gtk.Box               builder "upload-box"+  videoPreviewDrawingArea           <- builderGetObject GI.Gtk.DrawingArea       builder "video-preview-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"+  firstFrameImage                   <- builderGetObject GI.Gtk.Image             builder "first-frame-image"+  lastFrameImage                    <- builderGetObject GI.Gtk.Image             builder "last-frame-image"+  longGifGtkMessageDialog           <- builderGetObject GI.Gtk.MessageDialog     builder "long-gif-message-dialog"+  aboutDialog                       <- builderGetObject GI.Gtk.AboutDialog       builder "about-dialog"+  startTimeLevelBar                 <- builderGetObject GI.Gtk.LevelBar          builder "start-time-level-bar"+  endTimeLevelBar                   <- builderGetObject GI.Gtk.LevelBar          builder "end-time-level-bar"+  saveSpinner                       <- builderGetObject GI.Gtk.Spinner           builder "save-spinner"++  -- Glade does not allow us to use the response ID nicknames so we setup them up programmatically here.+  GI.Gtk.dialogAddActionWidget longGifGtkMessageDialog yesGtkButton (enumToInt32 GI.Gtk.ResponseTypeYes)+  GI.Gtk.dialogAddActionWidget longGifGtkMessageDialog noGtkButton  (enumToInt32 GI.Gtk.ResponseTypeNo)++  (maybeVideoPreviewWidget, maybePlaybinElement) <-+    GuiPreview.buildVideoPreviewWidgetAndPlaybinElement++  temporaryDirectory <- Gifcurry.findOrCreateTemporaryDirectory++  guiPreviewStateRef <- newIORef GR.defaultGuiPreviewState++  inVideoPropertiesRef <- newIORef GR.defaultInVideoProperties++  let guiComponents =+        GR.GuiComponents+          { GR.window                            = window+          , GR.startTimeSpinButton               = startTimeSpinButton+          , GR.durationTimeSpinButton            = durationTimeSpinButton+          , GR.widthSizeSpinButton               = widthSizeSpinButton+          , GR.qualityPercentSpinButton          = qualityPercentSpinButton+          , GR.leftCropSpinButton                = leftCropSpinButton+          , GR.rightCropSpinButton               = rightCropSpinButton+          , GR.topCropSpinButton                 = topCropSpinButton+          , GR.bottomCropSpinButton              = bottomCropSpinButton+          , GR.inFileChooserButton               = inFileChooserButton+          , GR.outFileChooserButton              = outFileChooserButton+          , GR.fontChooserButton                 = fontChooserButton+          , GR.saveButton                        = saveButton+          , GR.openButton                        = openButton+          , GR.yesGtkButton                      = yesGtkButton+          , GR.noGtkButton                       = noGtkButton+          , GR.aboutButton                       = aboutButton+          , GR.giphyUploadButton                 = giphyUploadButton+          , GR.imgurUploadButton                 = imgurUploadButton+          , GR.saveAsVideoRadioButton            = saveAsVideoRadioButton+          , GR.widthQualityPercentToggleButton   = widthQualityPercentToggleButton+          , GR.cropToggleButton                  = cropToggleButton+          , GR.topBottomTextToggleButton         = topBottomTextToggleButton+          , GR.saveOpenToggleButton              = saveOpenToggleButton+          , GR.uploadToggleButton                = uploadToggleButton+          , 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.mainPreviewBox                    = mainPreviewBox+          , GR.imagesPreviewBox                  = imagesPreviewBox+          , GR.videoPreviewBox                   = videoPreviewBox+          , GR.videoPreviewOverlayChildBox       = videoPreviewOverlayChildBox+          , GR.widthQualityPercentBox            = widthQualityPercentBox+          , GR.cropSpinButtonsBox                = cropSpinButtonsBox+          , GR.topBottomTextFontChooserBox       = topBottomTextFontChooserBox+          , GR.saveOpenBox                       = saveOpenBox+          , GR.uploadBox                         = uploadBox+          , GR.videoPreviewDrawingArea           = videoPreviewDrawingArea+          , GR.firstFramePreviewImageDrawingArea = firstFramePreviewImageDrawingArea+          , GR.lastFramePreviewImageDrawingArea  = lastFramePreviewImageDrawingArea+          , GR.firstFrameImage                   = firstFrameImage+          , GR.lastFrameImage                    = lastFrameImage+          , GR.longGifGtkMessageDialog           = longGifGtkMessageDialog+          , GR.aboutDialog                       = aboutDialog+          , GR.startTimeLevelBar                 = startTimeLevelBar+          , GR.endTimeLevelBar                   = endTimeLevelBar+          , GR.saveSpinner                       = saveSpinner+          , GR.maybeVideoPreviewWidget           = maybeVideoPreviewWidget+          , GR.maybePlaybinElement               = maybePlaybinElement+          , GR.temporaryDirectory                = temporaryDirectory+          , GR.guiPreviewStateRef                = guiPreviewStateRef+          , GR.inVideoPropertiesRef              = inVideoPropertiesRef+          }++  _ <- hideWidgetsOnRealize guiComponents++  _ <- handleInputFileChoice guiComponents+  _ <- handleSpinButtons guiComponents+  _ <- handleSaveButtonClick guiComponents+  _ <- handleOpenButtonClick guiComponents+  _ <- handleDialogs guiComponents+  _ <- handleSidebarSectionToggleButtons guiComponents+  _ <- handleUploadButtons guiComponents+  _ <- handleWindow guiComponents+  _ <- handleGuiPreview guiComponents++  GI.Gtk.main++buildBuilder :: IO GI.Gtk.Builder+buildBuilder = do+  gladeFile <- getDataFileName "data/gui.glade"+  GI.Gtk.builderNewFromFile (pack gladeFile)++builderGetObject+  ::  (GI.GObject.GObject b, GI.Gtk.IsBuilder a)+  =>  (Data.GI.Base.ManagedPtr b -> b)+  -> a+  -> String+  -> IO b+builderGetObject objectTypeClass builder objectId =+  fromJust <$> GI.Gtk.builderGetObject builder (pack objectId) >>=+  GI.Gtk.unsafeCastTo objectTypeClass++handleInputFileChoice :: GR.GuiComponents -> IO ()+handleInputFileChoice+  guiComponents@GR.GuiComponents+    { GR.inFileChooserButton+    , GR.startTimeSpinButton+    , GR.durationTimeSpinButton+    , GR.leftCropSpinButton+    , GR.rightCropSpinButton+    , GR.topCropSpinButton+    , GR.bottomCropSpinButton+    , GR.widthSizeSpinButton+    , GR.qualityPercentSpinButton+    , GR.topTextEntry+    , GR.bottomTextEntry+    , GR.outFileNameEntry+    , GR.statusEntry+    , GR.inVideoPropertiesRef+    }+  =+  void $ GI.Gtk.onFileChooserButtonFileSet inFileChooserButton $ do+    inFilePath <- fileChooserButtonGetFilePath inFileChooserButton+    maybeVideoDuration <-+      Gifcurry.getVideoDurationInSeconds+        Gifcurry.defaultGifParams+          { Gifcurry.inputFile = inFilePath }+    maybeWidthHeight <-+      Gifcurry.getVideoWidthAndHeight+        Gifcurry.defaultGifParams+          { Gifcurry.inputFile = inFilePath }+    case (maybeVideoDuration, maybeWidthHeight) of+      (Just videoDuration', Just (width, height)) -> do+        atomicWriteIORef inVideoPropertiesRef $+          GR.defaultInVideoProperties+            { GR.inVideoDuration = videoDuration'+            , GR.inVideoWidth    = width+            , GR.inVideoHeight   = height+            }+        let videoDuration = float2Double videoDuration'+        let startTimeFraction = 0.25+        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+        _ <- updateStatusEntryAsync statusEntry 1 $+          Data.Text.concat+            [ "That video is about "+            , videoDurationText+            , " seconds long."+            ]+        return ()+      _ -> do+        atomicWriteIORef inVideoPropertiesRef GR.defaultInVideoProperties+        _ <- updateStartAndDurationTimeSpinButtonRanges guiComponents+        _ <- GI.Gtk.spinButtonSetValue startTimeSpinButton 0.0+        _ <- GI.Gtk.spinButtonSetValue durationTimeSpinButton 0.0+        _ <- updateStatusEntryAsync statusEntry 1 "Is that a video?"+        return ()+    syncStartAndDurationTimeSpinButtons guiComponents+    resetTextEntries+    resetWidthAndQualityPercentSpinButtons+    resetCropSpinButtons+  where+    resetTextEntries :: IO ()+    resetTextEntries = do+      let textEntries =+            [ topTextEntry+            , bottomTextEntry+            , outFileNameEntry+            ]+      mapM_+        (flip GI.Gtk.entrySetText "")+        textEntries+    resetWidthAndQualityPercentSpinButtons :: IO ()+    resetWidthAndQualityPercentSpinButtons = do+      GI.Gtk.spinButtonSetValue widthSizeSpinButton 500+      GI.Gtk.spinButtonSetValue qualityPercentSpinButton 100+    resetCropSpinButtons :: IO ()+    resetCropSpinButtons = do+      let spinButtons =+            [ leftCropSpinButton+            , rightCropSpinButton+            , topCropSpinButton+            , bottomCropSpinButton+            ]+      mapM_+        (flip GI.Gtk.spinButtonSetValue 0.0)+        spinButtons++handleSpinButtons :: GR.GuiComponents -> IO ()+handleSpinButtons+  guiComponents@GR.GuiComponents+    { GR.startTimeSpinButton+    , GR.durationTimeSpinButton+    , GR.widthSizeSpinButton+    , GR.qualityPercentSpinButton+    , GR.leftCropSpinButton+    , GR.rightCropSpinButton+    , GR.topCropSpinButton+    , GR.bottomCropSpinButton+    , GR.statusEntry+    , GR.inVideoPropertiesRef+    }+  = do+  _ <- GI.Gtk.onSpinButtonValueChanged+    startTimeSpinButton+    handleStartTimeSpinButton+  _ <- GI.Gtk.onSpinButtonValueChanged+    durationTimeSpinButton+    handleDurationTimeSpinButton+  _ <- GI.Gtk.onSpinButtonValueChanged+    widthSizeSpinButton+    handleWidthSizeSpinButton+  _ <- GI.Gtk.onSpinButtonValueChanged+    qualityPercentSpinButton+    handleQualityPercentSpinButton+  _ <- GI.Gtk.onSpinButtonValueChanged+    leftCropSpinButton+    (handleCropSpinButton leftCropSpinButton rightCropSpinButton "left")+  _ <- GI.Gtk.onSpinButtonValueChanged+    rightCropSpinButton+    (handleCropSpinButton rightCropSpinButton leftCropSpinButton "right")+  _ <- GI.Gtk.onSpinButtonValueChanged+    topCropSpinButton+    (handleCropSpinButton topCropSpinButton bottomCropSpinButton "top")+  _ <- GI.Gtk.onSpinButtonValueChanged+    bottomCropSpinButton+    (handleCropSpinButton bottomCropSpinButton topCropSpinButton "bottom")+  return ()+  where+    handleStartTimeSpinButton :: IO ()+    handleStartTimeSpinButton = do+      startTime <- double2Float <$> GI.Gtk.spinButtonGetValue startTimeSpinButton+      _ <- setSpinButtonFraction startTimeSpinButton+      if startTime < 0.0+        then do+          GI.Gtk.entrySetText statusEntry "The start time is wrong."+          highlightSpinButton startTimeSpinButton+        else do+          GI.Gtk.entrySetText statusEntry "Ready."+          unhighlightSpinButton startTimeSpinButton+      void $ syncStartAndDurationTimeSpinButtons guiComponents+    handleDurationTimeSpinButton :: IO ()+    handleDurationTimeSpinButton = do+      startTime <- double2Float <$> GI.Gtk.spinButtonGetValue startTimeSpinButton+      durationTime <- double2Float <$> GI.Gtk.spinButtonGetValue durationTimeSpinButton+      videoDuration <- inVideoDuration+      _ <- GI.Gtk.setEntryProgressFraction durationTimeSpinButton 0.5+      _ <- setSpinButtonFraction durationTimeSpinButton+      if+           durationTime < 0.0+        || durationTime > (videoDuration - startTime)+        || durationTime > videoDuration+        then do+          GI.Gtk.entrySetText statusEntry "The duration time is wrong."+          highlightSpinButton durationTimeSpinButton+        else do+          GI.Gtk.entrySetText statusEntry "Ready."+          unhighlightSpinButton durationTimeSpinButton+      void $ syncStartAndDurationTimeSpinButtons guiComponents+    handleWidthSizeSpinButton :: IO ()+    handleWidthSizeSpinButton = do+      widthSize <- double2Float <$> GI.Gtk.spinButtonGetValue widthSizeSpinButton+      _ <- setSpinButtonFraction widthSizeSpinButton+      if widthSize <= 0.0+        then do+          GI.Gtk.entrySetText statusEntry "The width size is wrong."+          highlightSpinButton widthSizeSpinButton+        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+        then do+          GI.Gtk.entrySetText statusEntry $ Data.Text.concat ["The ", t, " crop is wrong."]+          highlightSpinButton a+        else do+          GI.Gtk.entrySetText statusEntry "Ready."+          unhighlightSpinButton a+      syncCropSpinButtons a b+    syncCropSpinButtons :: GI.Gtk.SpinButton -> GI.Gtk.SpinButton -> IO ()+    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+        let newValue' = if newValue < 0.0 then 0.0 else newValue+        void $ GI.Gtk.spinButtonSetValue b newValue'+      return ()+    inVideoDuration :: IO Float+    inVideoDuration =+      GR.inVideoDuration <$> readIORef inVideoPropertiesRef++handleSaveButtonClick :: GR.GuiComponents -> IO ()+handleSaveButtonClick+  GR.GuiComponents+    { GR.inFileChooserButton+    , GR.outFileChooserButton+    , GR.outFileNameEntry+    , GR.saveButton+    , GR.openButton+    , GR.fontChooserButton+    , GR.saveAsVideoRadioButton+    , GR.startTimeSpinButton+    , GR.durationTimeSpinButton+    , GR.widthSizeSpinButton+    , GR.qualityPercentSpinButton+    , GR.leftCropSpinButton+    , GR.rightCropSpinButton+    , GR.topCropSpinButton+    , GR.bottomCropSpinButton+    , GR.topTextEntry+    , GR.bottomTextEntry+    , GR.statusEntry+    , GR.longGifGtkMessageDialog+    , GR.saveSpinner+    }+  =+  void $ GI.Gtk.onWidgetButtonReleaseEvent saveButton $ \ _ -> do+    inFilePath     <- fileChooserButtonGetFilePath inFileChooserButton+    startTime      <- double2Float <$> GI.Gtk.spinButtonGetValue startTimeSpinButton+    durationTime   <- double2Float <$> GI.Gtk.spinButtonGetValue durationTimeSpinButton+    widthSize      <- double2Float <$> GI.Gtk.spinButtonGetValue widthSizeSpinButton+    qualityPercent <- double2Float <$> GI.Gtk.spinButtonGetValue qualityPercentSpinButton+    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+    let params =+          Gifcurry.defaultGifParams+            { Gifcurry.inputFile      = inFilePath+            , Gifcurry.outputFile     = outFilePath+            , Gifcurry.saveAsVideo    = saveAsVideo+            , 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.leftCrop       = leftCrop+            , Gifcurry.rightCrop      = rightCrop+            , Gifcurry.topCrop        = topCrop+            , Gifcurry.bottomCrop     = bottomCrop+            }+    paramsValid <- Gifcurry.gifParamsValid params+    GI.Gtk.entrySetText statusEntry "Ready."+    if paramsValid+      then do+        longGifGtkMessageDialogResponse <-+          if durationTime >= durationTimeWarningLevel+            then GI.Gtk.dialogRun longGifGtkMessageDialog+            else return (enumToInt32 GI.Gtk.ResponseTypeYes)+        when (longGifGtkMessageDialogResponse == enumToInt32 GI.Gtk.ResponseTypeYes) $ do+          GI.Gtk.widgetSetSensitive saveButton False+          GI.Gtk.widgetSetSensitive openButton False+          GI.Gtk.widgetHide saveButton+          GI.Gtk.widgetShow saveSpinner+          GI.Gtk.setSpinnerActive saveSpinner True+          void $ forkOS $ do+            GtkMainSyncAsync.gtkMainAsync $+              GI.Gtk.entrySetText statusEntry "One GIF coming up!"+            result <- Gifcurry.gif params+            case result of+              Left _ ->+                GtkMainSyncAsync.gtkMainAsync $+                  GI.Gtk.entrySetText statusEntry "Didn't work. Check your settings."+              Right filePath -> do+                _ <-+                  forkOS $+                    GtkMainSyncAsync.gtkMainAsync $+                      openLocalFileWithDefaultProgram filePath+                GtkMainSyncAsync.gtkMainAsync $ GI.Gtk.entrySetText statusEntry "Ready."+            GtkMainSyncAsync.gtkMainAsync $ do+              GI.Gtk.setSpinnerActive saveSpinner False+              GI.Gtk.widgetHide saveSpinner+              GI.Gtk.widgetShow saveButton+              GI.Gtk.widgetSetSensitive saveButton True+              GI.Gtk.widgetSetSensitive openButton True+      else GI.Gtk.entrySetText statusEntry "The settings are wrong."+    return True++handleOpenButtonClick :: GR.GuiComponents -> IO ()+handleOpenButtonClick+  GR.GuiComponents+    { GR.openButton+    , GR.saveAsVideoRadioButton+    , GR.outFileChooserButton+    , GR.outFileNameEntry+    , GR.statusEntry+    }+  =+  void $GI.Gtk.onWidgetButtonReleaseEvent openButton $ \ _ -> do+    saveAsVideo <- GI.Gtk.toggleButtonGetActive saveAsVideoRadioButton+    outFilePath <- outFileChooserButtonGetFilePath outFileChooserButton outFileNameEntry+    let outFilePath' =+          Gifcurry.getOutputFileWithExtension $+          Gifcurry.defaultGifParams+            { Gifcurry.outputFile = outFilePath, Gifcurry.saveAsVideo = saveAsVideo }+    fileExists <- doesFileExist outFilePath'+    if fileExists+      then do+        GI.Gtk.entrySetText statusEntry "Ready."+        void $+          forkIO $+            GtkMainSyncAsync.gtkMainAsync $+              openLocalFileWithDefaultProgram outFilePath'+      else GI.Gtk.entrySetText statusEntry "Couldn't find the file. Check your settings."+    return True++handleDialogs :: GR.GuiComponents -> IO ()+handleDialogs+  GR.GuiComponents+    { GR.aboutDialog+    , GR.longGifGtkMessageDialog+    , GR.aboutButton+    }+  = do+  _ <- GI.Gtk.onWidgetButtonReleaseEvent+    aboutButton+    (\ _ -> GI.Gtk.dialogRun aboutDialog >> return True)+  _ <- GI.Gtk.onDialogResponse+    longGifGtkMessageDialog+    (\ _ -> GI.Gtk.widgetHide longGifGtkMessageDialog)+  _ <- GI.Gtk.onDialogResponse+    aboutDialog+    (\ _ -> GI.Gtk.widgetHide aboutDialog)+  return ()++-- GI.Gtk.Expander does not work.+-- While it visually hides its children, they are still present+-- in terms of activation. This prevents the children of+-- other expanded sections from working.+-- This workaround emulates the expander functionality.+handleSidebarSectionToggleButtons :: GR.GuiComponents -> IO ()+handleSidebarSectionToggleButtons+  GR.GuiComponents+    { GR.widthQualityPercentToggleButton+    , GR.cropToggleButton+    , GR.topBottomTextToggleButton+    , GR.saveOpenToggleButton+    , GR.uploadToggleButton+    , GR.widthQualityPercentBox+    , GR.cropSpinButtonsBox+    , GR.topBottomTextFontChooserBox+    , GR.saveOpenBox+    , GR.uploadBox+    }+  = do+  let toggleButtons =+        [ widthQualityPercentToggleButton+        , cropToggleButton+        , topBottomTextToggleButton+        , saveOpenToggleButton+        , uploadToggleButton+        ]+  let boxes =+        [ widthQualityPercentBox+        , cropSpinButtonsBox+        , topBottomTextFontChooserBox+        , saveOpenBox+        , uploadBox+        ]+  mapM_+    (\ (toggleButton, box) ->+      void $+        GI.Gtk.onToggleButtonToggled toggleButton $+          handleOnToggle+            toggleButton+            box+            toggleButtons+    ) $+    Data.List.zip toggleButtons boxes+  where+    handleOnToggle :: GI.Gtk.ToggleButton -> GI.Gtk.Box -> [GI.Gtk.ToggleButton] -> IO ()+    handleOnToggle toggleButton box toggleButtons = do+      active <- GI.Gtk.toggleButtonGetActive toggleButton+      if active+        then do+          GI.Gtk.widgetShow box+          mapM_+            (\ x ->+              isNotToggleButton x >>= \ b -> when b $ GI.Gtk.setToggleButtonActive x False+            )+            toggleButtons+        else+          GI.Gtk.widgetHide box+      where+        isNotToggleButton :: GI.Gtk.ToggleButton -> IO Bool+        isNotToggleButton x = do+          a <- GI.Gtk.widgetGetName toggleButton+          b <- GI.Gtk.widgetGetName x+          return $ a /= b++handleUploadButtons :: GR.GuiComponents -> IO ()+handleUploadButtons+  GR.GuiComponents+    { GR.giphyUploadButton+    , GR.imgurUploadButton+    }+  =+  mapM_+    (\ (button, link) ->+      GI.Gtk.onButtonClicked button $ openUriWithDefaultProgram link+    )+    [ (giphyUploadButton, "https://giphy.com/upload/")+    , (imgurUploadButton, "https://imgur.com/upload")+    ]++handleGuiPreview :: GR.GuiComponents -> IO ()+handleGuiPreview = GuiPreview.runGuiPreview++handleWindow :: GR.GuiComponents -> IO ()+handleWindow GR.GuiComponents { GR.window } = do+  -- Setting the window to resizable false causes the video+  -- to load at its natural size.+  -- This workaround locks the window size which is required+  -- to get accurate crop measurements.+  geometry <- GI.Gdk.newZeroGeometry+  GI.Gdk.setGeometryMinWidth  geometry 1+  GI.Gdk.setGeometryMaxWidth  geometry 1+  GI.Gdk.setGeometryMinHeight geometry 1+  GI.Gdk.setGeometryMaxHeight geometry 1+  GI.Gtk.windowSetGeometryHints+    window+    (Just window)+    (Just geometry)+    [GI.Gdk.WindowHintsMinSize, GI.Gdk.WindowHintsMaxSize]+  _ <- GI.Gtk.onWidgetDestroy window GI.Gtk.mainQuit+  GI.Gtk.widgetShowAll window++openLocalFileWithDefaultProgram :: String -> IO ()+openLocalFileWithDefaultProgram filePath = do+  fileExists <- doesFileExist filePath+  when fileExists $ openUriWithDefaultProgram filePath++openUriWithDefaultProgram :: String -> IO ()+openUriWithDefaultProgram uri = do+  maybeFileOpenCommand <- determineFileOpenCommand+  case maybeFileOpenCommand of+    Just fileOpenCommand ->+      void $ spawnCommand $ fileOpenCommand ++ " \"" ++ uri ++ "\""+    _ -> return ()+  where+    determineFileOpenCommand :: IO (Maybe String)+    determineFileOpenCommand = do+      uname  <- tryReadProcess "uname"   ["-a"]+      swVers <- tryReadProcess "sw_vers" []+      ver    <- tryReadProcess "ver"     []+      let linuxCommand   = "xdg-open"+      let macCommand     = "open"+      let windowsCommand = "explorer"+      case (uname, swVers, ver) of+        (Right s, _, _) ->+          case (containsText "darwin" s, containsText "linux" s) of+            (True, _)   -> return $ Just macCommand+            (_, True)   -> return $ Just linuxCommand+            _           -> return Nothing+        (_, Right s, _) -> returnIf "mac"     s macCommand+        (_, _, Right s) -> returnIf "windows" s windowsCommand+        _               -> return Nothing+      where+        tryReadProcess :: String -> [String] -> IO (Either IOError String)+        tryReadProcess process params = try $ readProcess process params []+        returnIf :: Text -> String -> String -> IO (Maybe String)+        returnIf needle haystack command =+          if containsText needle haystack+            then return $ Just command+            else return Nothing+        containsText :: Text -> String -> Bool+        containsText needle haystack =+          Data.Text.isInfixOf+            needle+            (Data.Text.toLower $ Data.Text.pack haystack)++outFileChooserButtonGetFilePath :: GI.Gtk.FileChooserButton -> GI.Gtk.Entry -> IO String+outFileChooserButtonGetFilePath outFileChooserButton outFileNameEntry = do+  filePath <- fileChooserButtonGetString outFileChooserButton+  fileName <- Data.Text.unpack . Data.Text.strip <$> GI.Gtk.entryGetText outFileNameEntry+  if Data.List.null filePath+    then return fileName+    else do+      isDirectory <- System.Directory.doesDirectoryExist filePath+      if isDirectory+        then return $ System.FilePath.combine filePath fileName+        else return fileName++highlightSpinButton :: GI.Gtk.SpinButton -> IO ()+highlightSpinButton = highlightWidget "spinbutton" " entry "++unhighlightSpinButton :: GI.Gtk.SpinButton -> IO ()+unhighlightSpinButton = unhighlightWidget "spinbutton" " entry "++highlightWidget :: GI.Gtk.IsWidget a => Text -> Text -> a -> IO ()+highlightWidget namePrefix namePostfix =+  styleWidget namePrefix namePostfix "{ background-color: #ff4757; color: #f1f2f6; }"++unhighlightWidget :: GI.Gtk.IsWidget a => Text -> Text -> a -> IO ()+unhighlightWidget namePrefix namePostfix =+  styleWidget namePrefix namePostfix "{}"++styleWidget :: GI.Gtk.IsWidget a => Text -> Text -> Text -> a -> IO()+styleWidget namePrefix namePostfix style widget = do+  name <- GI.Gtk.widgetGetName widget+  provider <- GI.Gtk.cssProviderGetDefault+  let css =+        Data.ByteString.Char8.pack $+        Data.Text.unpack $+        Data.Text.concat+          [ namePrefix+          , "#"+          , name+          , namePostfix+          , " "+          , style+          ]+  GI.Gtk.cssProviderLoadFromData provider css+  styleContext <- GI.Gtk.widgetGetStyleContext widget+  void $+    GI.Gtk.styleContextAddProvider+      styleContext+      provider+      (fromIntegral GI.Gtk.STYLE_PROVIDER_PRIORITY_USER :: Word32)++updateStatusEntryAsync :: GI.Gtk.Entry -> Word32 -> Text -> IO ()+updateStatusEntryAsync statusEntry seconds message =+  void $ GI.GLib.timeoutAdd+    GI.GLib.PRIORITY_DEFAULT+    seconds $ do+      GtkMainSyncAsync.gtkMainAsync $+        GI.Gtk.entrySetText statusEntry message+      return False++setSpinButtonFraction :: GI.Gtk.SpinButton -> IO ()+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++updateStartAndDurationTimeSpinButtonRanges :: GR.GuiComponents -> IO ()+updateStartAndDurationTimeSpinButtonRanges+  GR.GuiComponents+    { GR.startTimeSpinButton+    , GR.durationTimeSpinButton+    , GR.inVideoPropertiesRef+    }+  = do+  videoDuration <- float2Double . GR.inVideoDuration <$> readIORef inVideoPropertiesRef+  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+  return ()++syncStartAndDurationTimeSpinButtons :: GR.GuiComponents -> IO ()+syncStartAndDurationTimeSpinButtons+  guiComponents@GR.GuiComponents+    { GR.startTimeSpinButton+    , GR.durationTimeSpinButton+    , GR.inVideoPropertiesRef+    }+  = do+  startTime <- GI.Gtk.spinButtonGetValue startTimeSpinButton+  durationTime <- GI.Gtk.spinButtonGetValue durationTimeSpinButton+  videoDuration <- float2Double . GR.inVideoDuration <$> readIORef inVideoPropertiesRef+  let maxDurationTime =+        if videoDuration - startTime <= 0.0 then 0.0 else videoDuration - startTime+  let durationTime' =+        if durationTime > maxDurationTime then maxDurationTime else durationTime+  let startTime' = if startTime >= videoDuration then videoDuration else startTime+  _ <- updateStartAndDurationTimeSpinButtonRanges guiComponents+  _ <- GI.Gtk.spinButtonSetValue startTimeSpinButton startTime'+  _ <- GI.Gtk.spinButtonSetValue durationTimeSpinButton durationTime'+  updateStartAndDurationTimeSpinButtonFractions guiComponents+  updateStartAndEndTimeLevelBars guiComponents++updateStartAndEndTimeLevelBars :: GR.GuiComponents -> IO ()+updateStartAndEndTimeLevelBars+  GR.GuiComponents+    { GR.startTimeSpinButton+    , GR.durationTimeSpinButton+    , GR.startTimeLevelBar+    , GR.endTimeLevelBar+    , GR.inVideoPropertiesRef+    }+  = do+  startTime <- GI.Gtk.spinButtonGetValue startTimeSpinButton+  durationTime <- GI.Gtk.spinButtonGetValue durationTimeSpinButton+  videoDuration <- float2Double . GR.inVideoDuration <$> readIORef inVideoPropertiesRef+  let endTime = startTime + durationTime+  let endTime' = videoDuration - endTime+  _ <- GI.Gtk.levelBarSetMinValue startTimeLevelBar 0.0+  _ <- GI.Gtk.levelBarSetMinValue endTimeLevelBar 0.0+  _ <- GI.Gtk.levelBarSetMaxValue startTimeLevelBar $ checkValue videoDuration+  _ <- GI.Gtk.levelBarSetMaxValue endTimeLevelBar $ checkValue videoDuration+  _ <- GI.Gtk.levelBarSetValue startTimeLevelBar $ checkValue startTime+  _ <- GI.Gtk.levelBarSetValue endTimeLevelBar $ checkValue endTime'+  return ()+  where+    checkValue :: Double -> Double+    checkValue v = if v <= levelBarBuffer then levelBarBuffer else v+    levelBarBuffer :: Double+    levelBarBuffer = 0.03++hideWidgetsOnRealize :: GR.GuiComponents -> IO ()+hideWidgetsOnRealize+  GR.GuiComponents+    { GR.saveSpinner+    , GR.widthQualityPercentBox+    , GR.cropSpinButtonsBox+    , GR.topBottomTextFontChooserBox+    , GR.saveOpenBox+    , GR.uploadBox+    }+  = do+  hideOnRealize saveSpinner+  let boxes =+        [ widthQualityPercentBox+        , cropSpinButtonsBox+        , topBottomTextFontChooserBox+        , saveOpenBox+        , uploadBox+        ]+  mapM_ hideOnRealize boxes+  where+    hideOnRealize :: GI.Gtk.IsWidget a => a -> IO ()+    hideOnRealize w = void $ GI.Gtk.onWidgetRealize w $ GI.Gtk.widgetHide w
src/lib/Gifcurry.hs view
@@ -4,19 +4,26 @@   lettier.com -} -{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE+    OverloadedStrings+  , NamedFieldPuns+#-}  -- | Produces GIFs using FFmpeg and ImageMagick. -- The main function is 'gif'.-module Gifcurry (-      gif-    , GifParams(..)-    , defaultGifParams-    , gifParamsValid-    , versionNumber-    , getVideoDurationInSeconds-    , findOrCreateTemporaryDirectory-  ) where+module Gifcurry+  ( gif+  , GifParams(..)+  , defaultGifParams+  , defaultFontChoice+  , gifParamsValid+  , versionNumber+  , getVideoDurationInSeconds+  , getOutputFileWithExtension+  , getVideoWidthAndHeight+  , findOrCreateTemporaryDirectory+  )+where  import System.Process import System.IO.Temp@@ -26,14 +33,17 @@ 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+data GifParams =+  GifParams+    { inputFile :: String     , outputFile :: String+    , saveAsVideo :: Bool     , startTime :: Float     , durationTime :: Float     , widthSize :: Int@@ -41,17 +51,24 @@     , fontChoice :: String     , topText :: String     , bottomText :: String-  } deriving (Show, Read)+    , leftCrop :: Float+    , rightCrop :: Float+    , topCrop :: Float+    , bottomCrop :: Float+    }+  deriving (Show, Read)  -- | The version number. versionNumber :: String-versionNumber = "2.3.0.0"+versionNumber = "3.0.0.0"  -- | Specifies default parameters for 'startTime', 'durationTime', 'widthSize', 'qualityPercent', and 'fontChoice'. defaultGifParams :: GifParams-defaultGifParams = GifParams {-      inputFile = ""+defaultGifParams =+  GifParams+    { inputFile = ""     , outputFile = ""+    , saveAsVideo = False     , startTime = 0.0     , durationTime = 1.0     , widthSize = 500@@ -59,7 +76,11 @@     , fontChoice = defaultFontChoice     , topText = ""     , bottomText = ""-  }+    , leftCrop = 0.0+    , rightCrop = 0.0+    , topCrop = 0.0+    , bottomCrop = 0.0+    }  -- | Inputs 'GifParams' and outputs either an IO IOError or IO String. --@@ -76,68 +97,135 @@ --        else return () -- @ gif :: GifParams -> IO (Either IOError String)-gif gifParams = do+gif+  gifParams@GifParams { saveAsVideo }+  = do   temporaryDirectory <- findOrCreateTemporaryDirectory-  withTempDirectory temporaryDirectory "gifcurry-frames" $ \tmpdir -> do-    printGifParams gifParams tmpdir-    validParams <- gifParamsValid gifParams-    if validParams-      then do-        fFMpegResult <- tryFfmpeg gifParams tmpdir-        let fFMpegSuccess = eitherBool fFMpegResult-        if fFMpegSuccess-          then do-            fontMatch <- getFontMatch gifParams-            let gifParams' = gifParams { fontChoice = fontMatch }-            putStrLn $ "Writing your GIF to: " ++-                       outputFile gifParams-            convertResult <- tryConvert gifParams' tmpdir-            let convertSuccess = eitherBool convertResult-            if convertSuccess-              then putStrLn "Done."-              else putStrLn "[Error] Something went wrong with ImageMagick."-            return convertResult-          else do-            putStrLn "[Error] Something went wrong with FFmpeg."-            return fFMpegResult-      else return $ Left (userError "[Error] Invalid params.")+  withTempDirectory temporaryDirectory "gifcurry-frames" $ \ tempDir ->+        handleFrameExtraction tempDir+    >>= handleFrameMerge tempDir+    >>= handleGifToVideoConversion   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+      case result of+        Left x -> do+          putStrLn "[ERROR] Something went wrong with ImageMagick."+          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) =+      if saveAsVideo+        then do+          result <- convertGifToVideo gifParams gifFilePath+          case result of+            Left x -> do+              putStrLn "[ERROR] Something went wrong with FFmpeg."+              return $ Left x+            Right outputFileWithExtension -> do+              putStrLn "[INFO] All done."+              return $ Right outputFileWithExtension+        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 $ "Font matched: " ++ match+      putStrLn $ "[INFO] Your font choice matched to \"" ++ match ++ "\"."       return match     defaultFontMatch :: IO String-    defaultFontMatch = putStrLn "Using the default font." >> return defaultFontChoice-    eitherBool :: Either a b -> Bool-    eitherBool = either (const False) (const True)+    defaultFontMatch = putStrLn "[INFO] Using the default font." >> return defaultFontChoice --- | Outputs True or False if the parameters in the GifParams record are valid.--- Looks at 'inputFile', 'outputFile', 'startTime', 'durationTime', 'widthSize', and 'qualityPercent'.+-- | Outputs `True` or `False` if the parameters in the `GifParams` record are valid. gifParamsValid :: GifParams -> IO Bool-gifParamsValid GifParams {-      inputFile = ipf-    , outputFile = opf-    , startTime = st-    , durationTime = dt-    , widthSize = ws-    , qualityPercent = qp-    , topText = _-    , bottomText = _-  } = do-    inputFileExists <- case Prelude.length ipf of+gifParamsValid+  GifParams+    { inputFile+    , outputFile+    , startTime+    , durationTime+    , widthSize+    , qualityPercent+    , leftCrop+    , rightCrop+    , topCrop+    , bottomCrop+    }+  = do+  inputFileExists <-+    case Prelude.length inputFile of       0 -> return False-      _ -> doesFileExist ipf-    unless inputFileExists $ putStrLn "\n[Error] Input video file does not exist."-    let fileNameValid = (not . Data.Text.null . Data.Text.strip . Data.Text.pack . System.FilePath.takeBaseName) opf-    let fileNameExtValid = ".gif" == (Data.Text.toLower . Data.Text.pack . System.FilePath.takeExtension) opf-    let outputFileValid = fileNameValid && fileNameExtValid-    unless outputFileValid $ putStrLn "\n[Error] Output GIF file is invalid."-    let valid = inputFileExists && outputFileValid && (st >= 0.0) && (dt > 0.0) && (ws > 0) && (qp > 0.0)-    unless valid $ putStrLn "\n[Error] Invalid params."-    return valid+      _ -> 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 widthLeftRightCropSizeValid =+        (widthSize' - (widthSize' * (leftCrop / 100.0)) - (widthSize' * (rightCrop / 100.0))) >= 1.0+  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"+  unless bottomCropValid              $ printInvalid "Bottom Crop"+  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+  where+    cropValid :: Float -> Bool+    cropValid c = c >= 0.0 && c <= 100.0+    printInvalid :: String -> IO ()+    printInvalid s = printError $ s ++ " is invalid."+    printError :: String -> IO ()+    printError s = putStrLn $ "[ERROR] " ++ s  -- | Returns the duration of the video in seconds if successful. --@@ -151,12 +239,71 @@ --                      Just float -> float -- @ getVideoDurationInSeconds :: GifParams -> IO (Maybe Float)-getVideoDurationInSeconds gifParams = tryFfprobe gifParams >>= result+getVideoDurationInSeconds GifParams { inputFile } = tryFfprobe params >>= result   where     result :: Either IOError String -> IO (Maybe Float)     result (Left _)               = return Nothing     result (Right durationString) = return (readMaybe durationString :: Maybe Float)+    params :: [String]+    params =+      [ "-i"+      , inputFile+      , "-v"+      , "quiet"+      , "-show_entries"+      , "format=duration"+      , "-of"+      , "default=noprint_wrappers=1:nokey=1"+      ] +-- | Returns the width and height of the video if successful.+-- If the width and/or height of the video is <= 0, it will+-- return nothing.+getVideoWidthAndHeight :: GifParams -> IO (Maybe (Float, Float))+getVideoWidthAndHeight GifParams { inputFile } = tryFfprobe params >>= result+  where+    result :: Either IOError String -> IO (Maybe (Float, Float))+    result (Left _)                  = return Nothing+    result (Right widthHeightString) =+      case (maybeWidth, maybeHeight) of+        (Just width, Just height) ->+          if width >= 0.0 && height > 0.0+            then return $ Just (width, height)+            else return Nothing+        _                         -> return Nothing+      where+        maybeWidth :: Maybe Float+        maybeWidth =+          case widthHeightTexts of+            (widthText:_) -> maybeFloat widthText+            _             -> Nothing+        maybeHeight :: Maybe Float+        maybeHeight =+          case widthHeightTexts of+            (_:heightText:_) -> maybeFloat heightText+            _                -> Nothing+        maybeFloat :: Text -> Maybe Float+        maybeFloat t = readMaybe (Data.Text.unpack t) :: Maybe Float+        widthHeightTexts :: [Text]+        widthHeightTexts =+          (Data.List.map Data.Text.strip . Data.Text.lines) widthHeightText+        widthHeightText :: Text+        widthHeightText =+          Data.Text.strip $ Data.Text.pack widthHeightString+    params :: [String]+    params =+      [ "-i"+      , inputFile+      , "-v"+      , "error"+      , "-select_streams"+      , "v:0"+      , "-show_entries"+      , "stream=width,height"+      , "-of"+      , "default=noprint_wrappers=1:nokey=1"+      ]+ -- | Finds or creates the temporary directory for Gifcurry. -- This directory is used for storing temporary frames. findOrCreateTemporaryDirectory :: IO FilePath@@ -165,121 +312,284 @@   System.Directory.createDirectoryIfMissing True filePath   return filePath -printGifParams :: GifParams -> String -> IO ()+-- | Adds the proper file extension to the 'outputFile' depending on 'saveAsVideo'.+getOutputFileWithExtension :: GifParams -> String+getOutputFileWithExtension GifParams { outputFile, saveAsVideo } =+      outputFile+  ++  "."+  ++  (if saveAsVideo then videoExtension else gifExtension)++-- | Returns the default font choice used if no font choice is specified.+defaultFontChoice :: String+defaultFontChoice = "sans"++gifOutputFile :: String -> String+gifOutputFile outputFile =+  getOutputFileWithExtension $+  defaultGifParams { outputFile = outputFile, saveAsVideo = False }++videoOutputFile :: String -> String+videoOutputFile outputFile =+  getOutputFileWithExtension $+  defaultGifParams { outputFile = outputFile, saveAsVideo = True }++defaultFrameRate :: Float+defaultFrameRate = 12.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 {-      inputFile = ipf-    , outputFile = opf-    , startTime = st-    , durationTime = dt-    , widthSize = ws-    , qualityPercent = qp-    , fontChoice = fc-    , topText = tt-    , bottomText = bt-  }-  tmpdir = mapM_ putStrLn [-        "\n----------------------------------------\n"-      , "Input file: " ++ ipf-      , "Output file: " ++ opf-      , "Start second: " ++ printf "%.3f" st-      , "Duration: " ++ printf "%.3f" dt ++ " seconds"-      , "GIF width: " ++ show ws ++ "px"-      , "Quality: " ++ show (qualityPercentClamp qp) ++ "%"-      , "Font Choice: " ++ fc-      , "Top text: " ++ tt-      , "Bottom text: " ++ bt+  gifParams@GifParams+    { inputFile+    , saveAsVideo+    , startTime+    , durationTime+    , widthSize+    , qualityPercent+    , fontChoice+    , topText+    , bottomText+    , leftCrop+    , rightCrop+    , topCrop+    , bottomCrop+    }+  =+  putStrLn $+    Prelude.unlines+      [ "[INFO] Here are your settings."       , ""-      , "Writing temporary frames to: " ++ tmpdir-    ]+      , "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+      ]+  where+    printFloat :: Float -> String+    printFloat = printf "%.3f"  frameFileExtension :: String frameFileExtension = "png" -tryFfmpeg :: GifParams -> String -> IO (Either IOError String)-tryFfmpeg-  GifParams {-      inputFile = ipf-    , startTime = st-    , durationTime = dt-    , widthSize = ws-  }-  tmpdir = try(readProcess "ffmpeg" params [])-  where-    sts = printf "%.3f" st-    dts = printf "%.3f" dt-    wss = show ws-    params = [-          "-nostats"-        , "-loglevel"-        , "panic"-        , "-an"-        , "-ss"-        , sts-        , "-i"-        , ipf-        , "-t"-        , dts-        , "-r"-        , "12"-        , "-q:v"-        , "31"-        , "-vf"-        , "scale=" ++ wss ++ ":-1"-        , "-f"-        , "image2"-        , tmpdir ++ "/%010d." ++ frameFileExtension-      ]+gifExtension :: String+gifExtension = "gif" -tryFfprobe :: GifParams -> IO (Either IOError String)-tryFfprobe GifParams { inputFile = ipf } = try(readProcess "ffprobe" params [])+videoExtension :: String+videoExtension = "mp4"++extractFrames :: GifParams -> String -> Float -> IO (Either IOError String)+extractFrames+  GifParams+    { inputFile+    , startTime+    , durationTime+    , widthSize+    , leftCrop+    , rightCrop+    , topCrop+    , bottomCrop+    }+  tempDir+  frameRate+  = do+  putStrLn $ "[INFO] Writing the temporary frames to: " ++ tempDir+  try $ readProcess "ffmpeg" params []   where-    params = [-          "-i"-        , ipf-        , "-v"-        , "quiet"-        , "-show_entries"-        , "format=duration"-        , "-of"-        , "default=noprint_wrappers=1:nokey=1"+    startTime' :: String+    startTime' = printf "%.3f" startTime+    durationTime' :: String+    durationTime' = printf "%.3f" durationTime+    widthSize' :: String+    widthSize' = show widthSize+    frameRate' :: String+    frameRate' = show $ maybeFrameRateOrDefaultFrameRate (Just frameRate)+    params :: [String]+    params =+      [ "-nostats"+      , "-loglevel"+      , "fatal"+      , "-an"+      , "-ss"+      , startTime'+      , "-i"+      , inputFile+      , "-t"+      , durationTime'+      , "-r"+      , frameRate'+      , "-q:v"+      , "31"+      , "-vf"+      , "scale="+        ++ widthSize'+        ++ ":-1"+        ++",crop=w=iw*(1-"+        ++ show ((leftCrop + rightCrop) / 100.0)+        ++ "):h=ih*(1-"+        ++ show ((topCrop + bottomCrop) / 100.0)+        ++ "):x=iw*"+        ++ show (leftCrop / 100.0)+        ++ ":y=ih*"+        ++ show (topCrop / 100.0)+        ++ ":exact=1"+      , "-f"+      , "image2"+      , tempDir ++ "/%010d." ++ frameFileExtension       ] -tryConvert :: GifParams -> String -> IO (Either IOError String)-tryConvert-  GifParams {-      outputFile = opf-    , qualityPercent = qp-    , fontChoice = fc-    , topText = tt-    , bottomText = bt-  }-  tmpdir = do-    maybeWidthHeight <- maybeGetFirstFrameFilePath tmpdir >>= maybeGetFirstFrameWidthHeight-    let params = [-                    "-quiet"-                  , "-delay"-                  , "8.3"-                  , tmpdir ++ "/*." ++ frameFileExtension-                  , "-coalesce"-                  , "-colors"-                  , show $ ncolors qp-                  , "-dither"-                  , "FloydSteinberg"-                  , "-layers"-                  , "remove-dups"-                  , "-layers"-                  , "compare-any"-                  , "-layers"-                  , "optimize-transparency"-                  , "-loop"-                  , "0"-                ]-                ++ annotate fc maybeWidthHeight tt "north"-                ++ annotate fc maybeWidthHeight bt "south"-                ++ [opf]-    try (readProcess "convert" params [])+mergeFramesIntoGif :: GifParams -> String -> Float -> IO (Either IOError String)+mergeFramesIntoGif+  GifParams+    { outputFile+    , saveAsVideo+    , qualityPercent+    , fontChoice+    , topText+    , bottomText+    }+  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 params =+            [ "-quiet"+            , "-delay"+            , delay+            , tempDir ++ "/*." ++ frameFileExtension+            , "-coalesce"+            , "-colors"+            , show $ ncolors qualityPercent+            , "-dither"+            , "FloydSteinberg"+            , "-layers"+            , "remove-dups"+            , "-layers"+            , "compare-any"+            , "-layers"+            , "optimize-transparency"+            , "-loop"+            , "0"+            ]+        ++  annotate fontChoice maybeWidthHeight topText "north"+        ++  annotate fontChoice maybeWidthHeight bottomText "south"+        ++  [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+  let outputFile' = videoOutputFile outputFile+  let params =+        [ "-nostats"+        , "-loglevel"+        , "fatal"+        , "-y"+        , "-i"+        , gifFilePath+        , "-movflags"+        , "faststart"+        , "-pix_fmt"+        , "yuv420p"+        , "-vf"+        , "scale=trunc(iw/2)*2:trunc(ih/2)*2"+        , outputFile'+        ]+  putStrLn $ "[INFO] Saving your video to: " ++ outputFile'+  result <- try $ readProcess "ffmpeg" params []+  if isLeft result+    then return result+    else return (Right outputFile')+ qualityPercentClamp :: Float -> Float qualityPercentClamp qp   | qp > 100.0   = 100.0@@ -292,36 +602,39 @@   | qpc >= 100.0 = 256   | otherwise  = truncate (qpc / 100.0 * 256.0)   where+    qpc :: Float     qpc = qualityPercentClamp qp  annotate :: String -> Maybe (Int, Int) -> String -> String -> [String]-annotate fontChoiceArg maybeWidthHeight text topBottom = [-      "-gravity"-    , topBottom-  ] ++ 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+annotate fontChoiceArg maybeWidthHeight text topBottom =+  [ "-gravity"+  , topBottom   ]+  ++ 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+  ]   where     pointsize :: String     pointsize = show $ pointSize maybeWidthHeight text@@ -385,15 +698,17 @@ 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+  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 tmpdir = try (makeAbsolute tmpdir) >>= tryListDir >>= maybeFirstFilePath+maybeGetFirstFrameFilePath tempDir =+  try (makeAbsolute tempDir) >>= tryListDir >>= maybeFirstFilePath   where     tryListDir :: Either IOError FilePath -> IO (FilePath, Either IOError [FilePath])     tryListDir (Left y) = return ("", Left y)@@ -407,16 +722,21 @@ maybeGetFirstFrameWidthHeight Nothing = return Nothing maybeGetFirstFrameWidthHeight (Just dir) =   readProcessWithExitCode "identify" [dir] [] >>=-    \ (_, stdout, _) -> maybeConvertWidthHeightString $ findWidthHeightString $ splitOn " " $ Data.Text.pack stdout+    \ (_, stdout, _) ->+      maybeConvertWidthHeightString $+      findWidthHeightString $+      splitOn " " $+      Data.Text.pack stdout   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+    maybeConvertWidthHeightString s =+      if Prelude.length splitOnX == 2+        then return (Just (pluckWidth splitOnX, pluckHeight splitOnX))+        else return Nothing       where         splitOnX :: [Text]         splitOnX = splitOn "x" $ Data.Text.toLower s@@ -427,13 +747,11 @@         pluckHeight (_:y:_) = read (Data.Text.unpack y) :: Int         pluckHeight _       = 0 -defaultFontChoice :: String-defaultFontChoice = "sans"- fontChoiceOrDefault :: GifParams -> String fontChoiceOrDefault GifParams { fontChoice = fontName } =   if Data.List.null cleanedFontName     then defaultFontChoice     else cleanedFontName   where+    cleanedFontName :: String     cleanedFontName = (Data.Text.unpack . Data.Text.strip . Data.Text.pack) fontName