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 +33/−0
- Gifcurry.cabal +26/−11
- README.md +277/−172
- lib/GiCairoCairoBridge.hs +31/−0
- lib/GtkMainSyncAsync.hs +7/−5
- makefile +11/−4
- src/cli/Main.hs +167/−91
- src/data/gifcurry-icon.png binary
- src/data/gifcurry-icon.svg +139/−0
- src/data/gifcurry-logo.svg +181/−109
- src/data/gui.glade +1224/−525
- src/gui/GuiMisc.hs +66/−0
- src/gui/GuiPreview.hs +591/−0
- src/gui/GuiRecords.hs +104/−0
- src/gui/Main.hs +867/−333
- src/lib/Gifcurry.hs +529/−211
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+# 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. -+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. - -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?++++++++## Got any sample GIFs?+++++## 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 & 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 & 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 & 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