├── .clang-format ├── .clang-tidy ├── .commitlintrc.yml ├── .github └── workflows │ ├── build.yml │ └── lint.yml ├── .gitignore ├── .versionrc ├── .vscode ├── c_cpp_properties.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── docs └── images │ ├── screenshot-1.0.0.png │ └── screenshot-beta.png ├── example └── config ├── include ├── algebra.h ├── application.h ├── box.h ├── clipboard.h ├── config.h ├── file.h ├── paint.h ├── pixbuf.h ├── render.h ├── swappy.h └── util.h ├── meson.build ├── meson_options.txt ├── package.json ├── res ├── icons │ └── hicolor │ │ └── scalable │ │ └── apps │ │ └── swappy.svg ├── meson.build ├── style │ └── swappy.css ├── swappy.glade └── swappy.gresource.xml ├── script ├── bump-meson-build.js └── github-release ├── src ├── algebra.c ├── application.c ├── box.c ├── clipboard.c ├── config.c ├── file.c ├── main.c ├── paint.c ├── pixbuf.c ├── po │ ├── LINGUAS │ ├── POTFILES │ ├── de.po │ ├── en.po │ ├── fr.po │ ├── meson.build │ ├── pt_BR.po │ ├── swappy.desktop.in │ ├── swappy.pot │ ├── tr.po │ └── zh_CN.po ├── render.c └── util.c ├── swappy.1.scd └── test └── images ├── heart-transparent.png ├── large.png ├── passwords.png └── small-blue.png /.clang-format: -------------------------------------------------------------------------------- 1 | # Use the Google style in this project. 2 | BasedOnStyle: Google 3 | IndentWidth: 2 4 | 5 | DerivePointerAlignment: false 6 | PointerAlignment: Right 7 | -------------------------------------------------------------------------------- /.clang-tidy: -------------------------------------------------------------------------------- 1 | --- 2 | Checks: 'clang-diagnostic-*,clang-analyzer-*,-clang-analyzer-security.insecureAPI.DeprecatedOrUnsafeBufferHandling' 3 | WarningsAsErrors: '' 4 | HeaderFilterRegex: '' 5 | FormatStyle: none 6 | CheckOptions: 7 | - key: cert-dcl16-c.NewSuffixes 8 | value: 'L;LL;LU;LLU' 9 | - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField 10 | value: '0' 11 | - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors 12 | value: '1' 13 | - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic 14 | value: '1' 15 | - key: google-readability-braces-around-statements.ShortStatementLines 16 | value: '1' 17 | - key: google-readability-function-size.StatementThreshold 18 | value: '800' 19 | - key: google-readability-namespace-comments.ShortNamespaceLines 20 | value: '10' 21 | - key: google-readability-namespace-comments.SpacesBeforeComments 22 | value: '2' 23 | - key: modernize-loop-convert.MaxCopySize 24 | value: '16' 25 | - key: modernize-loop-convert.MinConfidence 26 | value: reasonable 27 | - key: modernize-loop-convert.NamingStyle 28 | value: CamelCase 29 | - key: modernize-pass-by-value.IncludeStyle 30 | value: llvm 31 | - key: modernize-replace-auto-ptr.IncludeStyle 32 | value: llvm 33 | - key: modernize-use-nullptr.NullMacros 34 | value: 'NULL' 35 | ... 36 | 37 | -------------------------------------------------------------------------------- /.commitlintrc.yml: -------------------------------------------------------------------------------- 1 | --- 2 | extends: 3 | - "@commitlint/config-conventional" 4 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | build-and-lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - name: "Checkout repository" 10 | uses: actions/checkout@v4 11 | 12 | - name: Install dependencies 13 | run: | 14 | sudo apt-get update 15 | sudo apt --yes install libgtk-3-dev gettext meson ninja-build scdoc clang clang-format clang-tidy 16 | 17 | - name: "Build with gcc" 18 | run: | 19 | pkg-config --list-all 20 | CC=gcc meson setup build 21 | ninja -C build 22 | 23 | - name: "Build with clang" 24 | run: | 25 | git clean -fdx 26 | CC=clang meson setup build 27 | ninja -C build 28 | 29 | - name: "Lint with clang-format" 30 | run: | 31 | echo "Making sure clang-format is correct..." 32 | git ls-files -- '*.[ch]' | xargs clang-format -Werror -n 33 | echo "Running clang-tidy..." 34 | run-clang-tidy -p build 35 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Commitlint 2 | on: [pull_request] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-latest 7 | name: "Commitlint" 8 | steps: 9 | - name: "Checkout repository" 10 | uses: actions/checkout@v4 11 | with: 12 | fetch-depth: 0 13 | 14 | - name: "Commitlint" 15 | uses: wagoid/commitlint-github-action@v6 16 | with: 17 | configFile: "./.commitlintrc.yml" 18 | failOnWarnings: true 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Object files 5 | *.o 6 | *.ko 7 | *.obj 8 | *.elf 9 | 10 | # Linker output 11 | *.ilk 12 | *.map 13 | *.exp 14 | 15 | # Precompiled Headers 16 | *.gch 17 | *.pch 18 | 19 | # Libraries 20 | *.lib 21 | *.a 22 | *.la 23 | *.lo 24 | 25 | # Shared objects (inc. Windows DLLs) 26 | *.dll 27 | *.so 28 | *.so.* 29 | *.dylib 30 | 31 | # Executables 32 | *.exe 33 | *.out 34 | *.app 35 | *.i*86 36 | *.x86_64 37 | *.hex 38 | 39 | # Debug files 40 | *.dSYM/ 41 | *.su 42 | *.idb 43 | *.pdb 44 | 45 | # Kernel Module Compile Results 46 | *.mod* 47 | *.cmd 48 | .tmp_versions/ 49 | modules.order 50 | Module.symvers 51 | Mkfile.old 52 | dkms.conf 53 | 54 | # Temporary files 55 | *.ui~ 56 | 57 | # Build folders 58 | build/ 59 | release/ 60 | .cache/ 61 | -------------------------------------------------------------------------------- /.versionrc: -------------------------------------------------------------------------------- 1 | { 2 | "bumpFiles": [ 3 | { 4 | "filename": "meson.build", 5 | "updater": "script/bump-meson-build.js" 6 | }, 7 | { 8 | "filename": "package.json" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/c_cpp_properties.json: -------------------------------------------------------------------------------- 1 | { 2 | "configurations": [ 3 | { 4 | "name": "Linux", 5 | "defines": [], 6 | "compilerPath": "/usr/bin/clang", 7 | "cStandard": "c11", 8 | "cppStandard": "c++17", 9 | "intelliSenseMode": "clang-x64", 10 | "compileCommands": "${workspaceFolder}/build/compile_commands.json", 11 | "compilerArgs": ["build"], 12 | "includePath": [ 13 | "/usr/include/cairo/", 14 | "/usr/include/gdk-pixbuf-2.0/", 15 | "/usr/include/gio-unix-2.0/", 16 | "/usr/include/glib-2.0/", 17 | "/usr/include/gtk-3.0/", 18 | "/usr/include/pango-1.0/", 19 | "/usr/lib/clang/11.0.1/include/" 20 | ] 21 | } 22 | ], 23 | "version": 4 24 | } 25 | -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "swappy - geometry", 9 | "type": "cppdbg", 10 | "request": "launch", 11 | "program": "${workspaceFolder}/build/swappy", 12 | "args": ["-g", "0,0 200x200"], 13 | "stopAtEntry": false, 14 | "cwd": "${workspaceFolder}", 15 | "environment": [ 16 | { 17 | "name": "G_MESSAGES_DEBUG", 18 | "value": "all" 19 | } 20 | ], 21 | "externalConsole": false, 22 | "preLaunchTask": "build", 23 | "MIMode": "gdb", 24 | "setupCommands": [ 25 | { 26 | "description": "Enable pretty-printing for gdb", 27 | "text": "-enable-pretty-printing", 28 | "ignoreFailures": true 29 | } 30 | ] 31 | }, 32 | { 33 | "name": "swappy - file", 34 | "type": "cppdbg", 35 | "request": "launch", 36 | "program": "${workspaceFolder}/build/swappy", 37 | "args": ["-f", "docs/images/screenshot-beta.png"], 38 | "stopAtEntry": false, 39 | "cwd": "${workspaceFolder}", 40 | "environment": [ 41 | { 42 | "name": "G_MESSAGES_DEBUG", 43 | "value": "all" 44 | } 45 | ], 46 | "externalConsole": false, 47 | "preLaunchTask": "build", 48 | "MIMode": "gdb", 49 | "setupCommands": [ 50 | { 51 | "description": "Enable pretty-printing for gdb", 52 | "text": "-enable-pretty-printing", 53 | "ignoreFailures": true 54 | } 55 | ] 56 | }, 57 | { 58 | "name": "swappy - file (small blue)", 59 | "type": "cppdbg", 60 | "request": "launch", 61 | "program": "${workspaceFolder}/build/swappy", 62 | "args": ["-f", "test/images/small-blue.png"], 63 | "stopAtEntry": false, 64 | "cwd": "${workspaceFolder}", 65 | "environment": [ 66 | { 67 | "name": "G_MESSAGES_DEBUG", 68 | "value": "all" 69 | } 70 | ], 71 | "externalConsole": false, 72 | "preLaunchTask": "build", 73 | "MIMode": "gdb", 74 | "setupCommands": [ 75 | { 76 | "description": "Enable pretty-printing for gdb", 77 | "text": "-enable-pretty-printing", 78 | "ignoreFailures": true 79 | } 80 | ] 81 | }, 82 | { 83 | "name": "swappy - file (large)", 84 | "type": "cppdbg", 85 | "request": "launch", 86 | "program": "${workspaceFolder}/build/swappy", 87 | "args": ["-f", "test/images/large.png"], 88 | "stopAtEntry": false, 89 | "cwd": "${workspaceFolder}", 90 | "environment": [ 91 | { 92 | "name": "G_MESSAGES_DEBUG", 93 | "value": "all" 94 | } 95 | ], 96 | "externalConsole": false, 97 | "preLaunchTask": "build", 98 | "MIMode": "gdb", 99 | "setupCommands": [ 100 | { 101 | "description": "Enable pretty-printing for gdb", 102 | "text": "-enable-pretty-printing", 103 | "ignoreFailures": true 104 | } 105 | ] 106 | }, 107 | { 108 | "name": "swappy - file & stdout", 109 | "type": "cppdbg", 110 | "request": "launch", 111 | "program": "${workspaceFolder}/build/swappy", 112 | "args": ["-f", "docs/images/screenshot-beta.png", "-o", "-"], 113 | "stopAtEntry": false, 114 | "cwd": "${workspaceFolder}", 115 | "environment": [ 116 | { 117 | "name": "G_MESSAGES_DEBUG", 118 | "value": "all" 119 | } 120 | ], 121 | "externalConsole": false, 122 | "preLaunchTask": "build", 123 | "MIMode": "gdb", 124 | "setupCommands": [ 125 | { 126 | "description": "Enable pretty-printing for gdb", 127 | "text": "-enable-pretty-printing", 128 | "ignoreFailures": true 129 | } 130 | ] 131 | }, 132 | { 133 | "name": "swappy - stdin", 134 | "type": "cppdbg", 135 | "request": "launch", 136 | "program": "${workspaceFolder}/build/swappy", 137 | "args": ["-f", "-"], 138 | "stopAtEntry": false, 139 | "cwd": "${workspaceFolder}", 140 | "environment": [ 141 | { 142 | "name": "G_MESSAGES_DEBUG", 143 | "value": "all" 144 | } 145 | ], 146 | "externalConsole": false, 147 | "preLaunchTask": "build", 148 | "MIMode": "gdb", 149 | "setupCommands": [ 150 | { 151 | "description": "Enable pretty-printing for gdb", 152 | "text": "-enable-pretty-printing", 153 | "ignoreFailures": true 154 | } 155 | ] 156 | } 157 | ] 158 | } 159 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "mesonbuild.configureOnOpen": true, 3 | "files.associations": { 4 | "*.h": "c", 5 | "*.c": "c" 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "build", 6 | "type": "shell", 7 | "command": "ninja", 8 | "group": { 9 | "kind": "build", 10 | "isDefault": true 11 | }, 12 | "options": { 13 | "cwd": "${workspaceFolder}/build" 14 | }, 15 | "problemMatcher": { 16 | "base": "$gcc", 17 | "fileLocation": ["relative", "${workspaceFolder}/build"] 18 | } 19 | } 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ## [1.8.0](https://github.com/jtheoof/swappy/compare/v1.7.1...v1.8.0) (2025-08-27) 6 | 7 | 8 | ### Features 9 | 10 | * **config:** add left hand friendly keybinds ([e3b8bf6](https://github.com/jtheoof/swappy/commit/e3b8bf610fa64d21e053a8e25fb74acda334ba3d)) 11 | * **ui:** add transparency feature ([0416812](https://github.com/jtheoof/swappy/commit/0416812d63453d3f23e1e1e354882032ef86590b)) 12 | * **ui:** support input method when edit text ([5e3808c](https://github.com/jtheoof/swappy/commit/5e3808c7a3631c948c701a34de455cfaa3550757)) 13 | * **ui:** text supports pasting using Ctrl-v ([59dca02](https://github.com/jtheoof/swappy/commit/59dca0244906cf276a69ed11f21b5ed635690766)) 14 | 15 | 16 | ### Bug Fixes 17 | 18 | * **application:** bring back output option ([061155c](https://github.com/jtheoof/swappy/commit/061155c9dee96c6dc4e7c3a4b4dc03d45e7fe192)), closes [#202](https://github.com/jtheoof/swappy/issues/202) 19 | * **application:** save twice if auto_save and -o option ([e5cde68](https://github.com/jtheoof/swappy/commit/e5cde680e6c5180e35c80c0868effda1b5b8b1d9)) 20 | 21 | ### [1.7.1](https://github.com/jtheoof/swappy/compare/v1.7.0...v1.7.1) (2025-08-18) 22 | 23 | 24 | ### Bug Fixes 25 | 26 | * **render:** fix drawing transparent images ([49f84fd](https://github.com/jtheoof/swappy/commit/49f84fd681bc7cb707796e19086a51f7668ee228)) 27 | 28 | ## [1.7.0](https://github.com/jtheoof/swappy/compare/v1.5.1...v1.7.0) (2025-08-18) 29 | 30 | 31 | ### Features 32 | 33 | * auto saving on quit app ([72a511c](https://github.com/jtheoof/swappy/commit/72a511c435e9e605b8269669df02996c6b2b5e10)) 34 | * **config:** custom_color config option ([726159d](https://github.com/jtheoof/swappy/commit/726159d81b073f13c6e7661392c0440d4af162bf)) 35 | * **i18n:** add zh_CN locale ([9e7f3b2](https://github.com/jtheoof/swappy/commit/9e7f3b2bbc5aa43723284d4ecfc75807f920cb74)) 36 | 37 | 38 | ### Bug Fixes 39 | 40 | * **config:** fix segfault at exit ([182322d](https://github.com/jtheoof/swappy/commit/182322dd5bbdc6779052a949f7813c5c348123e3)) 41 | * **ui:** add non-versioned fontawesome ([3941fec](https://github.com/jtheoof/swappy/commit/3941feccf0720836c23566c7fcf9a96b27a62a37)) 42 | 43 | ## [1.6.0](https://github.com/jtheoof/swappy/compare/v1.5.1...v1.6.0) (2025-08-18) 44 | 45 | 46 | ### Features 47 | 48 | * auto saving on quit app ([72a511c](https://github.com/jtheoof/swappy/commit/72a511c435e9e605b8269669df02996c6b2b5e10)) 49 | * **config:** custom_color config option ([726159d](https://github.com/jtheoof/swappy/commit/726159d81b073f13c6e7661392c0440d4af162bf)) 50 | * **i18n:** add zh_CN locale ([9e7f3b2](https://github.com/jtheoof/swappy/commit/9e7f3b2bbc5aa43723284d4ecfc75807f920cb74)) 51 | 52 | 53 | ### Bug Fixes 54 | 55 | * **config:** fix segfault at exit ([182322d](https://github.com/jtheoof/swappy/commit/182322dd5bbdc6779052a949f7813c5c348123e3)) 56 | * **ui:** add non-versioned fontawesome ([3941fec](https://github.com/jtheoof/swappy/commit/3941feccf0720836c23566c7fcf9a96b27a62a37)) 57 | 58 | ### [1.5.1](https://github.com/jtheoof/swappy/compare/v1.5.0...v1.5.1) (2022-11-20) 59 | 60 | 61 | ### Bug Fixes 62 | 63 | * **ui:** use *-symbolic variant of toolbar icons ([5dc44f8](https://github.com/jtheoof/swappy/commit/5dc44f8970b0f6cdf21466bc2689ec2aa93a4385)), closes [#34](https://github.com/jtheoof/swappy/issues/34) 64 | 65 | ## [1.5.0](https://github.com/jtheoof/swappy/compare/v1.4.0...v1.5.0) (2022-11-18) 66 | 67 | 68 | ### Features 69 | 70 | * **config:** add early_exit option ([60da549](https://github.com/jtheoof/swappy/commit/60da5491e243c9edd85f6225326a68ae5e3edfd5)) 71 | * **config:** allow paint_mode to be configured through config file ([2f35f02](https://github.com/jtheoof/swappy/commit/2f35f02b4e89bf67b6e9cc461e874331d8ce2a4c)) 72 | * **config:** try to create `save_dir` if it does not exist ([4fb291a](https://github.com/jtheoof/swappy/commit/4fb291ad4b0b116afeaa7094b040083111b74674)) 73 | * **ui:** allow filling rectangles and ellipsis ([8ee55f7](https://github.com/jtheoof/swappy/commit/8ee55f7d52ce6ac71752981863f5795fef460049)), closes [#120](https://github.com/jtheoof/swappy/issues/120) 74 | 75 | ## [1.4.0](https://github.com/jtheoof/swappy/compare/v1.3.1...v1.4.0) (2021-09-06) 76 | 77 | 78 | ### Features 79 | 80 | * **draw:** draw shape from center if holding control ([d80c361](https://github.com/jtheoof/swappy/commit/d80c3614895d3b5da479831c651cc1afa2fcf916)) 81 | * **i18n:** add french translations ([cacb283](https://github.com/jtheoof/swappy/commit/cacb2830e4cc41010d6ab96655054d2eb1651651)) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * **desktop:** remove annotation from desktop categories ([0d383f6](https://github.com/jtheoof/swappy/commit/0d383f690b99026c340eab1efa590c48d54e7368)) 87 | * **desktop:** various fixes ([42425c0](https://github.com/jtheoof/swappy/commit/42425c0657a65b3f66ba4f64b1727c8198a70684)) 88 | * **i18n:** add german translations to desktop file ([c6b09e5](https://github.com/jtheoof/swappy/commit/c6b09e56399369b14a8de090a2239350dbe4aca8)) 89 | * **i18n:** add turkish translation to desktop file ([fa5769e](https://github.com/jtheoof/swappy/commit/fa5769e9406b8ab1b67aca3bff2656850362491e)) 90 | * **i18n:** properly set translation domain during layout init ([5301aeb](https://github.com/jtheoof/swappy/commit/5301aebd5e5534453621db7168b8afac5d7810f2)), closes [#92](https://github.com/jtheoof/swappy/issues/92) 91 | * **pixbuf:** handle invalid input file ([cdbd06d](https://github.com/jtheoof/swappy/commit/cdbd06d7af94b4aedfc2bda2231da8853f775f3a)) 92 | * **pixbuf:** handle overflow when filename_format is too long ([185575b](https://github.com/jtheoof/swappy/commit/185575bf75281eba8a0bc49b3da59225bdd9e1c7)), closes [#74](https://github.com/jtheoof/swappy/issues/74) 93 | * **po:** update GETTEXT_PACKAGE value with project name ([7fd552e](https://github.com/jtheoof/swappy/commit/7fd552e8c41f29711212d7f70edf61ac6ada7a7d)) 94 | * **release:** properly check sha256 remote content ([91985c7](https://github.com/jtheoof/swappy/commit/91985c7994764f52c8e9d864db8ec9cf2eb1df5c)), closes [#90](https://github.com/jtheoof/swappy/issues/90) 95 | 96 | ### [1.3.1](https://github.com/jtheoof/swappy/compare/v1.3.0...v1.3.1) (2021-02-20) 97 | 98 | ## [1.3.0](https://github.com/jtheoof/swappy/compare/v1.2.1...v1.3.0) (2021-02-18) 99 | 100 | 101 | ### Features 102 | 103 | * **cli:** add configure options for filename save ([597f005](https://github.com/jtheoof/swappy/commit/597f0055b9c6230b25a7f7a7bf3f4e14c06b1fbb)) 104 | * **i18n:** add brazilian portuguese translations ([4a0eb82](https://github.com/jtheoof/swappy/commit/4a0eb82369a0859fafdcce9d242c086cd2360a84)) 105 | * **i18n:** add german translations ([b4be847](https://github.com/jtheoof/swappy/commit/b4be8476350771454b29b9ce29c62a3337acc736)) 106 | * **i18n:** add turkish translations ([c8419da](https://github.com/jtheoof/swappy/commit/c8419da7faef14223ada6853942a6d11e2acf92f)) 107 | 108 | 109 | ### Bug Fixes 110 | 111 | * **application:** unlink temp file coming from stdin ([c24e56a](https://github.com/jtheoof/swappy/commit/c24e56a165394e60b37534287e168e5d8e69627c)), closes [#80](https://github.com/jtheoof/swappy/issues/80) 112 | * **blur:** optimize blur to only render after commit ([27fcece](https://github.com/jtheoof/swappy/commit/27fcecedaeea49aaec6acdecbc51cbd865a13363)) 113 | * **blur:** rgb24 is properly handled ([c04ed63](https://github.com/jtheoof/swappy/commit/c04ed63d26e5012215198f7b41a7f2232dac1ebe)) 114 | * **clipboard:** wl-copy mimetype should be png ([a931acb](https://github.com/jtheoof/swappy/commit/a931acb2cff615badc63294ed121aba008f32ef8)), closes [#68](https://github.com/jtheoof/swappy/issues/68) 115 | * **notification:** notification shows the image icon ([eb53e5c](https://github.com/jtheoof/swappy/commit/eb53e5c2b28717f509dd58eab6da85897c0d6d9d)) 116 | * **ui:** adjust rendering surface with proper scaling ([9b72571](https://github.com/jtheoof/swappy/commit/9b72571596f9313d4efd94a4b17da8b3733fd2de)), closes [#54](https://github.com/jtheoof/swappy/issues/54) 117 | * **ui:** commit state before copying or saving ([46e5854](https://github.com/jtheoof/swappy/commit/46e5854b3cce93a82984b19ca90e3f3337952fe2)), closes [#52](https://github.com/jtheoof/swappy/issues/52) 118 | * **ui:** compute window sizes and buffers properly ([5bcffdb](https://github.com/jtheoof/swappy/commit/5bcffdbb01cc6e56f9c0f37de899b46efe68ed4a)), closes [#56](https://github.com/jtheoof/swappy/issues/56) 119 | 120 | ### [1.2.1](https://github.com/jtheoof/swappy/compare/v1.2.0...v1.2.1) (2020-07-11) 121 | 122 | 123 | ### Bug Fixes 124 | 125 | * **text:** properly handle utf-8 chars ([717ab0c](https://github.com/jtheoof/swappy/commit/717ab0c2d1757e10bb4eef17d35ccd6a991705c4)), closes [#43](https://github.com/jtheoof/swappy/issues/43) 126 | 127 | ## [1.2.0](https://github.com/jtheoof/swappy/compare/v1.1.0...v1.2.0) (2020-07-05) 128 | 129 | 130 | ### Features 131 | 132 | * **i18n:** add translatable desktop file ([cf3d7a5](https://github.com/jtheoof/swappy/commit/cf3d7a5283a7b8c34b05996f87b608513e0830ca)), closes [#35](https://github.com/jtheoof/swappy/issues/35) 133 | * **i18n:** setup i18n for swappy ([5b3c8ad](https://github.com/jtheoof/swappy/commit/5b3c8aded8fd4f9d00aa660a24127de0e1791d7f)) 134 | 135 | ## [1.2.0](https://github.com/jtheoof/swappy/compare/v1.1.0...v1.2.0) (2020-07-05) 136 | 137 | 138 | ### Features 139 | 140 | * **i18n:** add translatable desktop file ([cf3d7a5](https://github.com/jtheoof/swappy/commit/cf3d7a5283a7b8c34b05996f87b608513e0830ca)), closes [#35](https://github.com/jtheoof/swappy/issues/35) 141 | * **i18n:** setup i18n for swappy ([5b3c8ad](https://github.com/jtheoof/swappy/commit/5b3c8aded8fd4f9d00aa660a24127de0e1791d7f)) 142 | 143 | ## [1.1.0](https://github.com/jtheoof/swappy/compare/v1.0.1...v1.1.0) (2020-06-23) 144 | 145 | 146 | ### Features 147 | 148 | * **cli:** add -v and --version flags ([e32c024](https://github.com/jtheoof/swappy/commit/e32c02454ae4ec6ac30549d5fa9e80c2b64edb72)) 149 | 150 | ### [1.0.1](https://github.com/jtheoof/swappy/compare/v1.0.0...v1.0.1) (2020-06-21) 151 | 152 | 153 | ### Bug Fixes 154 | 155 | * **cli:** stop showing -g option ([ee06d66](https://github.com/jtheoof/swappy/commit/ee06d6685f6f59ffce544b45d7b51f3f4523348b)) 156 | 157 | ## 1.0.0 (2020-06-21) 158 | 159 | 160 | ### ⚠ BREAKING CHANGES 161 | 162 | * We do no support the `-g` option anymore. 163 | 164 | This tool simply makes more sense as the output of `grim` rather than 165 | trying to be `grim`. 166 | 167 | RIP my ugly wayland code, long live maintainable code. 168 | 169 | Next stop, rust? 170 | 171 | ### Features 172 | 173 | * **ui:** life is full of colors and joy ([a8c8be3](https://github.com/jtheoof/swappy/commit/a8c8be37ca996f3e1b752bca67eee594706bc08f)) 174 | * init project ([efc3ecc](https://github.com/jtheoof/swappy/commit/efc3eccc9e21892a6b0979126a23d21d3d6a3b3d)) 175 | * **application:** print final surface to file or stdout ([196f7f4](https://github.com/jtheoof/swappy/commit/196f7f4dea3ab569f0523171ae7c424b8e8423ee)), closes [#2](https://github.com/jtheoof/swappy/issues/2) 176 | * **application:** update app ([ce27741](https://github.com/jtheoof/swappy/commit/ce27741017554d6606e23434273f55476bc8ae37)) 177 | * **blur:** add multiple passes logic ([f9737d7](https://github.com/jtheoof/swappy/commit/f9737d78c96a5d9f4566c94702c3ec4a41d9e219)) 178 | * **blur:** remove blur configuration ([361be6a](https://github.com/jtheoof/swappy/commit/361be6aa8085143d9fd721e4c315c6b9e6fbdfca)) 179 | * **blur:** use rect blur instead of brush ([1be7798](https://github.com/jtheoof/swappy/commit/1be7798a8bcfc494b20489e2e1f8b0245f4b5e84)), closes [#17](https://github.com/jtheoof/swappy/issues/17) 180 | * **buffer:** ability to read from stdin ([02bc464](https://github.com/jtheoof/swappy/commit/02bc46456453e8530a3c9f1289dfce7e71371945)) 181 | * **buffer:** add file image support ([f6c189c](https://github.com/jtheoof/swappy/commit/f6c189c7b7f35ca4da75abaac0bd85c3d5ce5b09)) 182 | * **clipboard:** use wl-copy if present ([51b27d7](https://github.com/jtheoof/swappy/commit/51b27d768eef7fbbdab365fa94a81af5395b0e3e)) 183 | * **config:** add show_panel config ([307f579](https://github.com/jtheoof/swappy/commit/307f57956f105d22de2d8242313517b6a79ed4e2)), closes [#12](https://github.com/jtheoof/swappy/issues/12) 184 | * **config:** have overridable defaults ([ef24851](https://github.com/jtheoof/swappy/commit/ef24851deec2d6b7f76ed0fbbcd31b54b336cae3)), closes [#1](https://github.com/jtheoof/swappy/issues/1) 185 | * **draw:** convert wl_shm_format to cairo_format ([c623939](https://github.com/jtheoof/swappy/commit/c623939e02238f053312ad6367e761aec254c6fe)) 186 | * **draw:** draw the screencopy buffer ([2344414](https://github.com/jtheoof/swappy/commit/2344414102789975e6ce425a95e8b96159cf51ba)) 187 | * **layer:** use geometry size ([290d3ca](https://github.com/jtheoof/swappy/commit/290d3ca230d32ec2ef4036bf9e32f1e711fecd84)) 188 | * **paint:** introduce text paint ([3347bf2](https://github.com/jtheoof/swappy/commit/3347bf23bf17d4c2cc8e5b9bbadd657efafb28e7)) 189 | * **screencopy:** add buffer creation through screencopy ([bff8687](https://github.com/jtheoof/swappy/commit/bff8687fc81ebb57a179b1f50300f9c0cda793e3)) 190 | * **screencopy:** introduce screencopy features ([53c9770](https://github.com/jtheoof/swappy/commit/53c977080829c7e816db1a9ec45eb432f6b7b354)) 191 | * **swappy:** copy to clipboard with CTRL+C ([b90500e](https://github.com/jtheoof/swappy/commit/b90500ed34defcb8ebc67965c4dbb5d068ee8049)) 192 | * **swappy:** introduce file option ([c56df33](https://github.com/jtheoof/swappy/commit/c56df33d1880d22372e21ef0ebf5dd8805d65a76)) 193 | * **swappy:** save to file with CTRL+S ([af0b1a1](https://github.com/jtheoof/swappy/commit/af0b1a11a21faac04f8b43c4c9ef616ab5fd2b78)) 194 | * **text:** add controls in toggle panel ([c03f628](https://github.com/jtheoof/swappy/commit/c03f628de793e170d9f62c5b786fe18891bb6fa3)) 195 | * **tool:** introduce blurring capability ([fae0aea](https://github.com/jtheoof/swappy/commit/fae0aeacab6fb28e17975097c8b4c5c7e5ad57fd)), closes [#17](https://github.com/jtheoof/swappy/issues/17) 196 | * **ui:** add binding for clear action ([2bdab68](https://github.com/jtheoof/swappy/commit/2bdab684e1eace53ad7b78414ad467d312dc10ad)) 197 | * **ui:** add binding to toggle panel ([e8d2f12](https://github.com/jtheoof/swappy/commit/e8d2f12ce1737fa19972e5c4109e1c85cc2b157e)) 198 | * **ui:** add keybindings for color change ([c5ec285](https://github.com/jtheoof/swappy/commit/c5ec285ee73ddf90df2cb571e1d6c61159605c8e)) 199 | * **ui:** add keybindings for stroke size ([562a9a6](https://github.com/jtheoof/swappy/commit/562a9a6e92201677f31de126b646c619caf33863)) 200 | * **ui:** add shortcuts for undo/redo ([d7e7f2b](https://github.com/jtheoof/swappy/commit/d7e7f2b5ffd46aa36bed6ecc6709aeb94cce64ae)) 201 | * **ui:** add toggle panel button ([7674d7d](https://github.com/jtheoof/swappy/commit/7674d7db8ba8d97302a045af8d2383de37acb2d1)), closes [#24](https://github.com/jtheoof/swappy/issues/24) 202 | * **ui:** add undo/redo ([bcc1314](https://github.com/jtheoof/swappy/commit/bcc13140ebfdefa30431b288f089d23bb1df743e)) 203 | * **ui:** life is full of colors and joy ([606cd34](https://github.com/jtheoof/swappy/commit/606cd3459de3908e5fecdb7a49162ef3a9b52ab7)) 204 | * **ui:** replace popover by on screen elements ([8cd3f13](https://github.com/jtheoof/swappy/commit/8cd3f134bbd8e05523303914f6c8f3989e6b4502)) 205 | * **wayland:** added xdg_output_manager ([7b3549f](https://github.com/jtheoof/swappy/commit/7b3549fdd86fe1a945e1988bf22042c0f8dd6ed0)) 206 | * **wayland:** listing outputs ([5a55c8b](https://github.com/jtheoof/swappy/commit/5a55c8bbbd08ad717ddabac51be31483950d827f)) 207 | 208 | 209 | ### Bug Fixes 210 | 211 | * **application:** fix file loop and use of GTK object after lifecycle ([320dae0](https://github.com/jtheoof/swappy/commit/320dae02d0c6dca3fa2fd7ca934a85483ac2dd35)) 212 | * **application:** memory leak for pixbuf ([f9d70fc](https://github.com/jtheoof/swappy/commit/f9d70fc0e22274e6cbe74bfdf714cdf04e34053d)) 213 | * **application:** properly save output file upon clean exit ([b5cc433](https://github.com/jtheoof/swappy/commit/b5cc433d75d77759cef139e0e232bde79196f886)), closes [#8](https://github.com/jtheoof/swappy/issues/8) 214 | * **application:** suffix saved file with png ([7f2f6da](https://github.com/jtheoof/swappy/commit/7f2f6da754571771475558233f5a47813ec278dd)) 215 | * **blur:** adjust blur bounding box based on scaled monitor ([6b2ec90](https://github.com/jtheoof/swappy/commit/6b2ec90efd99e1979310b673ad40b3724669dac1)) 216 | * **blur:** blur based on device scaling factor ([1699474](https://github.com/jtheoof/swappy/commit/1699474c39fc305492c8bb03063c4582af4dbf9e)) 217 | * **blur:** use better glyph icon ([97cd607](https://github.com/jtheoof/swappy/commit/97cd6072c986c9a7c69306744390a6ddb6a44646)) 218 | * **blur:** use rendered surface after commit ([46fb08d](https://github.com/jtheoof/swappy/commit/46fb08dce17a820fcb500d2b6ff02f7d682f3c18)), closes [#20](https://github.com/jtheoof/swappy/issues/20) [#22](https://github.com/jtheoof/swappy/issues/22) 219 | * **buffer:** properly include required functions ([d787586](https://github.com/jtheoof/swappy/commit/d787586b9ed1d7e855ae2d416914d619636f41b1)), closes [#10](https://github.com/jtheoof/swappy/issues/10) 220 | * **clipboard:** handle bad write to pipe fd ([f963a76](https://github.com/jtheoof/swappy/commit/f963a76c5c01b9b5f81b97118bf1b9e6990d995d)) 221 | * **clipboard:** memory leak for pixbuf ([665295b](https://github.com/jtheoof/swappy/commit/665295b497d7ef124d5a2eeb7eb76964fdb3566a)) 222 | * **dependencies:** include glib2 ([992d97e](https://github.com/jtheoof/swappy/commit/992d97e94d2ebd32ac3e1901910050fae1954ed0)), closes [#11](https://github.com/jtheoof/swappy/issues/11) 223 | * **file:** properly check file system errors if any ([541ec21](https://github.com/jtheoof/swappy/commit/541ec21ca0efdec4d06c96f5ad1768b4219ed4ab)) 224 | * **init:** fix segfault for unknown flags ([f4e9a19](https://github.com/jtheoof/swappy/commit/f4e9a19407d8d1bfa59c08f6bf97617c662e1ac0)) 225 | * **init:** properly handle null geometry ([c4ea305](https://github.com/jtheoof/swappy/commit/c4ea305ae6ac9429bf44fdfc7218a30363439582)) 226 | * **man:** remove blur_level related config ([ceb907a](https://github.com/jtheoof/swappy/commit/ceb907a5dc736c7d44318b35fb911aeb2360d851)) 227 | * **meson:** able to build on standard platforms ([8abc5d5](https://github.com/jtheoof/swappy/commit/8abc5d52ec2962a111c6d44cdb5e9e209ac219c7)) 228 | * **meson:** remove useless cname in meson res file ([9b8ea64](https://github.com/jtheoof/swappy/commit/9b8ea64307b33eb010b8ba043919f3eddf935b19)) 229 | * **paint:** fix memory leak for brush paints ([aed2bfe](https://github.com/jtheoof/swappy/commit/aed2bfe29465aa5161155c1edda9d03cac607906)) 230 | * **pixbuf:** possibly fix core dump ([8a82e79](https://github.com/jtheoof/swappy/commit/8a82e796bb871b57fa6ab4d2ed8d761033370d8c)) 231 | * **pixbuf:** properly grab pixbuf size from cairo surface ([2adcf94](https://github.com/jtheoof/swappy/commit/2adcf944f4a7f2da5b5edf49a37922c43b2e477e)), closes [#6](https://github.com/jtheoof/swappy/issues/6) 232 | * **render:** better handler empty buffer ([acf2379](https://github.com/jtheoof/swappy/commit/acf2379ba3117ba6eb8c426e85a60ce71a3abe67)) 233 | * **render:** draw from last to first ([4b69ada](https://github.com/jtheoof/swappy/commit/4b69ada9a1469d3b6e106e07bf7155836b31d613)) 234 | * **render:** fix arrow glitch with 0 ftx ([ec6e6ab](https://github.com/jtheoof/swappy/commit/ec6e6abae7629800fec4c715957c4932946f51ed)) 235 | * **render:** properly scale arrow along with stroke size ([75bfc10](https://github.com/jtheoof/swappy/commit/75bfc10fb7a5507b66bd6d19ab06f2f6a393bb6a)) 236 | * **resources:** compile resources and fix error management ([05d87c9](https://github.com/jtheoof/swappy/commit/05d87c929ff8b3311cd5db111cd2f53a32c35a19)) 237 | * **string:** fix algo to insert chars at location ([bc3264e](https://github.com/jtheoof/swappy/commit/bc3264e9f11bb4f3a02d7f5ae92ef8a4d2b42513)) 238 | * **ui:** add stroke size increase/decrease/reset ([5930c99](https://github.com/jtheoof/swappy/commit/5930c99b9e0208148d6bc8cf0fc3aa8f69dbd36d)) 239 | * **ui:** move paint area inside GtkFixed ([50e7c97](https://github.com/jtheoof/swappy/commit/50e7c97042805f5550d2a62d45c8e49208d7632d)) 240 | * **ui:** prevent focus in panel buttons ([903ad11](https://github.com/jtheoof/swappy/commit/903ad114f516981c8d0644f704af9c722f74a61f)) 241 | * **ui:** small tweaks ([2b73777](https://github.com/jtheoof/swappy/commit/2b73777142141598c14d37d1b6fa9573de12d914)) 242 | * **ui:** tweak button sizes ([425f455](https://github.com/jtheoof/swappy/commit/425f455ab7665a046060fe140c861aeb7ea8209b)) 243 | * **ui/render:** adjust rendering based on window size ([445980b](https://github.com/jtheoof/swappy/commit/445980bbf4702e59113fab506b2e9e36ad931666)), closes [#6](https://github.com/jtheoof/swappy/issues/6) 244 | * **wayland:** initialize done copies to 0 ([65cefc1](https://github.com/jtheoof/swappy/commit/65cefc1da7fed86508301250ffc1b6dbc9fd3692)) 245 | * **wayland:** replace g_error by g_warning ([64bfc2b](https://github.com/jtheoof/swappy/commit/64bfc2b3a71ed00d0dc1102501ac85792735833f)) 246 | * **window:** quit when delete event is received ([0c5e458](https://github.com/jtheoof/swappy/commit/0c5e458d4c44a2e2e2b4451b4576724aef2a06b0)) 247 | 248 | 249 | * refactor!(wayland): remove wayland code ([204a93e](https://github.com/jtheoof/swappy/commit/204a93eb0f696bc7be8335d46212c6024e3b2c51)) 250 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Jeremy Attali 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # swappy 2 | 3 | A Wayland native snapshot and editor tool, inspired by [Snappy] on macOS. Works great with [grim], [slurp] and [sway]. But can easily work with other screen copy tools that can output a final image to `stdout`. See [below](#example-usage). 4 | 5 | ## Screenshot 6 | 7 | ![Swappy Screenshot](docs/images/screenshot-1.0.0.png) 8 | 9 | ## Example usage 10 | 11 | Output of `grim` (or any tool outputting an image file): 12 | 13 | ```sh 14 | grim -g "$(slurp)" - | swappy -f - 15 | ``` 16 | 17 | Swappshot a PNG file: 18 | 19 | ```sh 20 | swappy -f "~/Desktop/my-gnome-saved-file.png" 21 | ``` 22 | 23 | Print final surface to stdout (useful to pipe with other tools): 24 | 25 | ```sh 26 | grim -g "$(slurp)" - | swappy -f - -o - | pngquant - 27 | ``` 28 | 29 | Grab a swappshot from a specific window under Sway, using `swaymsg` and `jq`: 30 | 31 | ```sh 32 | grim -g "$(swaymsg -t get_tree | jq -r '.. | select(.pid? and .visible?) | .rect | "\(.x),\(.y) \(.width)x\(.height)"' | slurp)" - | swappy -f - 33 | ``` 34 | 35 | ## Config 36 | 37 | The config file is located at `$XDG_CONFIG_HOME/swappy/config` or at `$HOME/.config/swappy/config`. 38 | 39 | The file follows the GLib `conf` format. See the `man` page for details. There is example config file [here](example/config). 40 | 41 | The following lines can be used as swappy's default: 42 | 43 | ``` 44 | [Default] 45 | save_dir=$HOME/Desktop 46 | save_filename_format=swappy-%Y%m%d-%H%M%S.png 47 | show_panel=false 48 | line_size=5 49 | text_size=20 50 | text_font=sans-serif 51 | paint_mode=brush 52 | early_exit=false 53 | fill_shape=false 54 | auto_save=false 55 | custom_color=rgba(193,125,17,1) 56 | transparent=false 57 | transparency=50 58 | ``` 59 | 60 | - `save_dir` is where swappshots will be saved, can contain env variables, when it does not exist, swappy attempts to create it first, but does not abort if directory creation fails 61 | - `save_filename_format`: is the filename template, if it contains a date format, this will be parsed into a timestamp. Format is detailed in [strftime(3)](https://man.archlinux.org/man/strftime.3). If this date format is missing, filename will have no timestamp 62 | - `show_panel` is used to toggle the paint panel on or off upon startup 63 | - `line_size` is the default line size (must be between 1 and 50) 64 | - `text_size` is the default text size (must be between 10 and 50) 65 | - `text_font` is the font used to render text, its format is pango friendly 66 | - `paint_mode` is the mode activated at application start (must be one of: brush|text|rectangle|ellipse|arrow|blur, matching is case-insensitive) 67 | - `early_exit` is used to make the application exit after saving the picture or copying it to the clipboard 68 | - `fill_shape` is used to toggle shape filling (for the rectangle and ellipsis tools) on or off upon startup 69 | - `auto_save` is used to toggle auto saving of final buffer to `save_dir` upon exit 70 | - `custom_color` is used to set a default value for the custom color 71 | - `transparency` is used to set transparency of everything that is drawn during startup 72 | - `transparent` is used to toggle transparency during startup 73 | 74 | 75 | ## Keyboard Shortcuts 76 | 77 | - `Ctrl+b`: Toggle Paint Panel 78 | 79 |
80 | 81 | - `b`: Switch to Brush 82 | - `e` `t`: Switch to Text (Editor) 83 | - `r` `s`: Switch to Rectangle (Square) 84 | - `c` `o`: Switch to Ellipse (Circle) 85 | - `a`: Switch to Arrow 86 | - `d`: Switch to Blur (`d` stands for droplet) 87 | 88 |
89 | 90 | - `R`: Use Red Color 91 | - `G`: Use Green Color 92 | - `B`: Use Blue Color 93 | - `C`: Use Custom Color 94 | - `Minus`: Reduce Stroke Size 95 | - `Plus`: Increase Stroke Size 96 | - `Equal`: Reset Stroke Size 97 | - `f`: Toggle Shape Filling 98 | - `x` `k`: Clear Paints (cannot be undone) 99 | - `T`: Toggle Transparency 100 | 101 |
102 | 103 | - `Ctrl`: Center Shape (Rectangle & Ellipse) based on draw start 104 | 105 |
106 | 107 | - `Ctrl+z`: Undo 108 | - `Ctrl+Shift+z` or `Ctrl+y`: Redo 109 | - `Ctrl+s`: Save to file (see man page) 110 | - `Ctrl+c`: Copy to clipboard 111 | - `Escape` or `q` or `Ctrl+w`: Quit swappy 112 | 113 | ## Limitations 114 | 115 | - **Copy**: If you don't have [wl-clipboard] installed, copy to clipboard won't work if you close swappy (the content of the clipboard is lost). This because GTK 3.24 [has not implemented persistent storage on wayland backend yet](https://gitlab.gnome.org/GNOME/gtk/blob/3.24.13/gdk/wayland/gdkdisplay-wayland.c#L857). We need to do it on the [Wayland level](https://github.com/swaywm/wlr-protocols/blob/master/unstable/wlr-data-control-unstable-v1.xml), or wait for GTK 4. For now, we use `wl-copy` if installed and revert to `gtk` clipboard if not found. 116 | - **Fonts**: Swappy relies on Font Awesome 5 being present to properly render the icons. On Arch you can simply install those with: `sudo pacman -S otf-font-awesome` 117 | - **Output Format**: Only PNG is supported. 118 | 119 | ## Installation 120 | 121 | - [Arch Linux](https://archlinux.org/packages/extra/x86_64/swappy/) 122 | - [Arch Linux (git)](https://aur.archlinux.org/packages/swappy-git) 123 | - [Fedora](https://src.fedoraproject.org/rpms/swappy) 124 | - [Gentoo](https://packages.gentoo.org/packages/gui-apps/swappy) 125 | - [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/swappy) 126 | - [Void Linux](https://github.com/void-linux/void-packages/tree/master/srcpkgs/swappy) 127 | 128 | ## Building from source 129 | 130 | Install dependencies (on Arch, name can vary for other distros): 131 | 132 | - meson 133 | - ninja 134 | - cairo 135 | - pango 136 | - gtk 137 | - glib2 138 | - scdoc 139 | 140 | Optional dependencies: 141 | 142 | - `wl-clipboard` (to make sure the copy is saved if you close swappy) 143 | - `otf-font-awesome` (to draw the paint icons properly) 144 | 145 | Then run: 146 | 147 | ```sh 148 | meson setup build 149 | ninja -C build 150 | ``` 151 | 152 | ### i18n 153 | 154 | This section is for developers, maintainers and translators. 155 | 156 | To add support to a new locale or when translations are updated: 157 | 158 | 1. Update `src/po/LINGUAS` (when new locales are added) 159 | 2. Generate a new `po` file (ignore and do not commit potential noise in other files): 160 | 161 | ```sh 162 | ninja -C build swappy-update-po 163 | ``` 164 | 165 | To rebuild the base template (should happen less often): 166 | 167 | ```sh 168 | ninja -C build swappy-pot 169 | ``` 170 | 171 | See the [meson documentation](https://mesonbuild.com/Localisation.html) for details. 172 | 173 | ## Contributing 174 | 175 | Pull requests are welcome. This project uses [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to automate changelog generation. 176 | 177 | ## Release 178 | 179 | We rely on [standard-version](https://github.com/conventional-changelog/standard-version) which is part of the JavaScript ecosystem but works well with any project. 180 | 181 | ```sh 182 | ./script/github-release 183 | ``` 184 | 185 | Make sure everything is valid in the Draft release, then publish the draft. 186 | 187 | Release tarballs are signed with this PGP key: `F44D05A50F6C9EB5C81BCF966A6B35DBE9442683` 188 | 189 | ## License 190 | 191 | MIT 192 | 193 | [snappy]: http://snappy-app.com/ 194 | [slurp]: https://github.com/emersion/slurp 195 | [grim]: https://github.com/emersion/grim 196 | [sway]: https://github.com/swaywm/sway 197 | [wl-clipboard]: https://github.com/bugaevc/wl-clipboard 198 | -------------------------------------------------------------------------------- /docs/images/screenshot-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/docs/images/screenshot-1.0.0.png -------------------------------------------------------------------------------- /docs/images/screenshot-beta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/docs/images/screenshot-beta.png -------------------------------------------------------------------------------- /example/config: -------------------------------------------------------------------------------- 1 | [Default] 2 | save_dir=$HOME/Desktop 3 | line_size=5 4 | text_size=20 5 | text_font=sans-serif 6 | -------------------------------------------------------------------------------- /include/algebra.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | struct gaussian_kernel { 6 | gdouble *kernel; 7 | gint size; 8 | gdouble sigma; 9 | gdouble sum; 10 | }; 11 | 12 | struct gaussian_kernel *gaussian_kernel(gint width, gdouble sigma); 13 | void gaussian_kernel_free(gpointer data); 14 | -------------------------------------------------------------------------------- /include/application.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "swappy.h" 4 | 5 | bool application_init(struct swappy_state *state); 6 | int application_run(struct swappy_state *state); 7 | void application_finish(struct swappy_state *state); 8 | 9 | /* Glade signals */ 10 | void window_keypress_handler(GtkWidget *widget, GdkEventKey *event, 11 | struct swappy_state *state); 12 | gboolean window_delete_handler(GtkWidget *widget, GdkEvent *event, 13 | struct swappy_state *state); 14 | 15 | void pane_toggled_handler(GtkWidget *widget, struct swappy_state *state); 16 | void undo_clicked_handler(GtkWidget *widget, struct swappy_state *state); 17 | void redo_clicked_handler(GtkWidget *widget, struct swappy_state *state); 18 | 19 | gboolean draw_area_handler(GtkWidget *widget, cairo_t *cr, 20 | struct swappy_state *state); 21 | gboolean draw_area_configure_handler(GtkWidget *widget, 22 | GdkEventConfigure *event, 23 | struct swappy_state *state); 24 | void draw_area_button_press_handler(GtkWidget *widget, GdkEventButton *event, 25 | struct swappy_state *state); 26 | void draw_area_button_release_handler(GtkWidget *widget, GdkEventButton *event, 27 | struct swappy_state *state); 28 | void draw_area_motion_notify_handler(GtkWidget *widget, GdkEventMotion *event, 29 | struct swappy_state *state); 30 | 31 | void brush_clicked_handler(GtkWidget *widget, struct swappy_state *state); 32 | void text_clicked_handler(GtkWidget *widget, struct swappy_state *state); 33 | void rectangle_clicked_handler(GtkWidget *widget, struct swappy_state *state); 34 | void ellipse_clicked_handler(GtkWidget *widget, struct swappy_state *state); 35 | void arrow_clicked_handler(GtkWidget *widget, struct swappy_state *state); 36 | void blur_clicked_handler(GtkWidget *widget, struct swappy_state *state); 37 | 38 | void copy_clicked_handler(GtkWidget *widget, struct swappy_state *state); 39 | void save_clicked_handler(GtkWidget *widget, struct swappy_state *state); 40 | void clear_clicked_handler(GtkWidget *widget, struct swappy_state *state); 41 | 42 | void color_red_clicked_handler(GtkWidget *widget, struct swappy_state *state); 43 | void color_green_clicked_handler(GtkWidget *widget, struct swappy_state *state); 44 | void color_blue_clicked_handler(GtkWidget *widget, struct swappy_state *state); 45 | 46 | void color_custom_clicked_handler(GtkWidget *widget, 47 | struct swappy_state *state); 48 | void color_custom_color_set_handler(GtkWidget *widget, 49 | struct swappy_state *state); 50 | 51 | void stroke_size_decrease_handler(GtkWidget *widget, 52 | struct swappy_state *state); 53 | void stroke_size_reset_handler(GtkWidget *widget, struct swappy_state *state); 54 | void stroke_size_increase_handler(GtkWidget *widget, 55 | struct swappy_state *state); 56 | 57 | void text_size_decrease_handler(GtkWidget *widget, struct swappy_state *state); 58 | void text_size_reset_handler(GtkWidget *widget, struct swappy_state *state); 59 | void text_size_increase_handler(GtkWidget *widget, struct swappy_state *state); 60 | 61 | void transparency_decrease_handler(GtkWidget *widget, 62 | struct swappy_state *state); 63 | void transparency_reset_handler(GtkWidget *widget, struct swappy_state *state); 64 | void transparency_increase_handler(GtkWidget *widget, 65 | struct swappy_state *state); 66 | -------------------------------------------------------------------------------- /include/box.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "swappy.h" 4 | 5 | bool box_parse(struct swappy_box *box, const char *str); 6 | bool is_empty_box(struct swappy_box *box); 7 | bool intersect_box(struct swappy_box *a, struct swappy_box *b); 8 | -------------------------------------------------------------------------------- /include/clipboard.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "swappy.h" 4 | 5 | bool clipboard_copy_drawing_area_to_selection(struct swappy_state *state); 6 | -------------------------------------------------------------------------------- /include/config.h: -------------------------------------------------------------------------------- 1 | #include "swappy.h" 2 | 3 | #define CONFIG_LINE_SIZE_DEFAULT 5 4 | #define CONFIG_TEXT_FONT_DEFAULT "sans-serif" 5 | #define CONFIG_TEXT_SIZE_DEFAULT 20 6 | #define CONFIG_TRANSPARENCY_DEFAULT 50 7 | #define CONFIG_SHOW_PANEL_DEFAULT false 8 | #define CONFIG_SAVE_FILENAME_FORMAT_DEFAULT "swappy-%Y%m%d_%H%M%S.png" 9 | #define CONFIG_PAINT_MODE_DEFAULT SWAPPY_PAINT_MODE_BRUSH 10 | #define CONFIG_EARLY_EXIT_DEFAULT false 11 | #define CONFIG_FILL_SHAPE_DEFAULT false 12 | #define CONFIG_AUTO_SAVE_DEFAULT false 13 | #define CONFIG_CUSTOM_COLOR_DEFAULT "rgba(193,125,17,1)" 14 | #define CONFIG_TRANSPARENT_DEFAULT false 15 | 16 | void config_load(struct swappy_state *state); 17 | void config_free(struct swappy_state *state); 18 | -------------------------------------------------------------------------------- /include/file.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | bool folder_exists(const char *path); 4 | bool file_exists(const char *path); 5 | char *file_dump_stdin_into_a_temp_file(); 6 | -------------------------------------------------------------------------------- /include/paint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | 5 | #include "swappy.h" 6 | 7 | void paint_add_temporary(struct swappy_state *state, double x, double y, 8 | enum swappy_paint_type type); 9 | void paint_update_temporary_shape(struct swappy_state *state, double x, 10 | double y, gboolean is_control_pressed); 11 | void paint_update_temporary_text(struct swappy_state *state, 12 | GdkEventKey *event); 13 | void paint_update_temporary_str(struct swappy_state *state, char *event); 14 | void paint_update_temporary_text_clip(struct swappy_state *state, gdouble x, 15 | gdouble y); 16 | void paint_commit_temporary(struct swappy_state *state); 17 | 18 | void paint_free(gpointer data); 19 | void paint_free_all(struct swappy_state *state); 20 | void paint_free_list(GList **list); 21 | -------------------------------------------------------------------------------- /include/pixbuf.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "swappy.h" 4 | 5 | GdkPixbuf *pixbuf_init_from_file(struct swappy_state *state); 6 | GdkPixbuf *pixbuf_get_from_state(struct swappy_state *state); 7 | void pixbuf_save_state_to_folder(GdkPixbuf *pixbuf, char *folder, 8 | char *filename_format); 9 | void pixbuf_save_to_file(GdkPixbuf *pixbuf, char *file); 10 | void pixbuf_save_to_stdout(GdkPixbuf *pixbuf); 11 | void pixbuf_scale_surface_from_widget(struct swappy_state *state, 12 | GtkWidget *widget); 13 | void pixbuf_free(struct swappy_state *state); 14 | -------------------------------------------------------------------------------- /include/render.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include "swappy.h" 4 | 5 | void render_state(struct swappy_state *state); 6 | -------------------------------------------------------------------------------- /include/swappy.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #define MAX_PATH 4096 10 | 11 | #define SWAPPY_LINE_SIZE_MIN 1 12 | #define SWAPPY_LINE_SIZE_MAX 50 13 | 14 | #define SWAPPY_TEXT_SIZE_MIN 10 15 | #define SWAPPY_TEXT_SIZE_MAX 50 16 | 17 | #define SWAPPY_TRANSPARENCY_MIN 5 18 | #define SWAPPY_TRANSPARENCY_MAX 95 19 | 20 | enum swappy_paint_type { 21 | SWAPPY_PAINT_MODE_BRUSH = 0, /* Brush mode to draw arbitrary shapes */ 22 | SWAPPY_PAINT_MODE_TEXT, /* Mode to draw texts */ 23 | SWAPPY_PAINT_MODE_RECTANGLE, /* Rectangle shapes */ 24 | SWAPPY_PAINT_MODE_ELLIPSE, /* Ellipse shapes */ 25 | SWAPPY_PAINT_MODE_ARROW, /* Arrow shapes */ 26 | SWAPPY_PAINT_MODE_BLUR, /* Blur mode */ 27 | }; 28 | 29 | enum swappy_paint_shape_operation { 30 | SWAPPY_PAINT_SHAPE_OPERATION_STROKE = 0, /* Used to stroke the shape */ 31 | SWAPPY_PAINT_SHAPE_OPERATION_FILL, /* Used to fill the shape */ 32 | }; 33 | 34 | enum swappy_text_mode { 35 | SWAPPY_TEXT_MODE_EDIT = 0, 36 | SWAPPY_TEXT_MODE_DONE, 37 | }; 38 | 39 | struct swappy_point { 40 | gdouble x; 41 | gdouble y; 42 | }; 43 | 44 | struct swappy_paint_text { 45 | double r; 46 | double g; 47 | double b; 48 | double a; 49 | double s; 50 | gchar *font; 51 | gchar *text; 52 | glong cursor; 53 | struct swappy_point from; 54 | struct swappy_point to; 55 | enum swappy_text_mode mode; 56 | }; 57 | 58 | struct swappy_paint_shape { 59 | double r; 60 | double g; 61 | double b; 62 | double a; 63 | double w; 64 | bool should_center_at_from; 65 | struct swappy_point from; 66 | struct swappy_point to; 67 | enum swappy_paint_type type; 68 | enum swappy_paint_shape_operation operation; 69 | }; 70 | 71 | struct swappy_paint_brush { 72 | double r; 73 | double g; 74 | double b; 75 | double a; 76 | double w; 77 | GList *points; 78 | }; 79 | 80 | struct swappy_paint_blur { 81 | struct swappy_point from; 82 | struct swappy_point to; 83 | cairo_surface_t *surface; 84 | }; 85 | 86 | struct swappy_paint { 87 | enum swappy_paint_type type; 88 | bool can_draw; 89 | bool is_committed; 90 | union { 91 | struct swappy_paint_brush brush; 92 | struct swappy_paint_shape shape; 93 | struct swappy_paint_text text; 94 | struct swappy_paint_blur blur; 95 | } content; 96 | }; 97 | 98 | struct swappy_box { 99 | int32_t x; 100 | int32_t y; 101 | int32_t width; 102 | int32_t height; 103 | }; 104 | 105 | struct swappy_state_settings { 106 | double r; 107 | double g; 108 | double b; 109 | double a; 110 | double w; 111 | double t; 112 | int32_t tr; 113 | }; 114 | 115 | struct swappy_state_ui { 116 | gboolean panel_toggled; 117 | 118 | GtkWindow *window; 119 | GtkIMContext *im_context; 120 | 121 | GtkWidget *area; 122 | 123 | GtkToggleButton *panel_toggle_button; 124 | 125 | // Undo / Redo 126 | GtkButton *undo; 127 | GtkButton *redo; 128 | 129 | // Painting Area 130 | GtkBox *painting_box; 131 | GtkRadioButton *brush; 132 | GtkRadioButton *text; 133 | GtkRadioButton *rectangle; 134 | GtkRadioButton *ellipse; 135 | GtkRadioButton *arrow; 136 | GtkRadioButton *blur; 137 | 138 | GtkRadioButton *red; 139 | GtkRadioButton *green; 140 | GtkRadioButton *blue; 141 | GtkRadioButton *custom; 142 | GtkColorButton *color; 143 | 144 | GtkButton *line_size; 145 | GtkButton *text_size; 146 | GtkButton *transparency; 147 | GtkButton *transparency_plus; 148 | GtkButton *transparency_minus; 149 | 150 | GtkToggleButton *fill_shape; 151 | GtkToggleButton *transparent; 152 | }; 153 | 154 | struct swappy_config { 155 | char *config_file; 156 | char *save_dir; 157 | char *save_filename_format; 158 | gint8 paint_mode; 159 | gboolean fill_shape; 160 | gboolean transparent; 161 | gboolean show_panel; 162 | guint32 line_size; 163 | guint32 text_size; 164 | guint32 transparency; 165 | char *text_font; 166 | gboolean early_exit; 167 | gboolean auto_save; 168 | char *custom_color; 169 | }; 170 | 171 | struct swappy_state { 172 | GtkApplication *app; 173 | 174 | struct swappy_state_ui *ui; 175 | struct swappy_config *config; 176 | 177 | GdkPixbuf *original_image; 178 | cairo_surface_t *original_image_surface; 179 | cairo_surface_t *rendering_surface; 180 | 181 | gdouble scaling_factor; 182 | 183 | enum swappy_paint_type mode; 184 | 185 | /* Options */ 186 | char *file_str; 187 | char *output_file; 188 | 189 | char *temp_file_str; 190 | 191 | struct swappy_box *window; 192 | struct swappy_box *geometry; 193 | 194 | GList *paints; 195 | GList *redo_paints; 196 | struct swappy_paint *temp_paint; 197 | 198 | struct swappy_state_settings settings; 199 | 200 | int argc; 201 | char **argv; 202 | }; 203 | -------------------------------------------------------------------------------- /include/util.h: -------------------------------------------------------------------------------- 1 | 2 | #pragma once 3 | 4 | #include 5 | 6 | glong string_get_nb_bytes_until(gchar *str, glong until); 7 | gchar *string_remove_at(char *str, glong pos); 8 | gchar *string_insert_chars_at(gchar *str, gchar *chars, glong pos); 9 | void pixel_data_print(guint32 pixel); 10 | -------------------------------------------------------------------------------- /meson.build: -------------------------------------------------------------------------------- 1 | project( 2 | 'swappy', 3 | 'c', 4 | version: '1.8.0', 5 | license: 'MIT', 6 | meson_version: '>=0.48.0', 7 | default_options: [ 8 | 'c_std=c11', 9 | 'warning_level=2', 10 | 'werror=true', 11 | ], 12 | ) 13 | 14 | version = '"@0@"'.format(meson.project_version()) 15 | git = find_program('git', native: true, required: false) 16 | if git.found() 17 | git_commit = run_command([git, 'rev-parse', '--short', 'HEAD'], check:true) 18 | git_branch = run_command([git, 'rev-parse', '--abbrev-ref', 'HEAD'], check:true) 19 | if git_commit.returncode() == 0 and git_branch.returncode() == 0 20 | version = '"@0@-@1@ (" __DATE__ ", branch \'@2@\')"'.format( 21 | meson.project_version(), 22 | git_commit.stdout().strip(), 23 | git_branch.stdout().strip(), 24 | ) 25 | endif 26 | endif 27 | add_project_arguments('-DSWAPPY_VERSION=@0@'.format(version), language: 'c') 28 | 29 | add_project_arguments('-Wno-unused-parameter', language: 'c') 30 | 31 | swappy_inc = include_directories('include') 32 | 33 | cc = meson.get_compiler('c') 34 | 35 | if cc.get_id() == 'clang' 36 | message('clang') 37 | add_global_arguments('-Wno-missing-field-initializers', language: 'c') 38 | endif 39 | 40 | cairo = dependency('cairo') 41 | pango = dependency('pango') 42 | math = cc.find_library('m') 43 | gtk = dependency('gtk+-3.0', version: '>=3.20.0') 44 | gio = dependency('gio-2.0') 45 | 46 | subdir('res') 47 | subdir('src/po') 48 | 49 | executable( 50 | 'swappy', 51 | swappy_resources, 52 | files([ 53 | 'src/main.c', 54 | 'src/algebra.c', 55 | 'src/application.c', 56 | 'src/box.c', 57 | 'src/config.c', 58 | 'src/clipboard.c', 59 | 'src/file.c', 60 | 'src/paint.c', 61 | 'src/pixbuf.c', 62 | 'src/render.c', 63 | 'src/util.c', 64 | ]), 65 | dependencies: [ 66 | cairo, 67 | pango, 68 | gio, 69 | gtk, 70 | math, 71 | ], 72 | link_args: '-rdynamic', 73 | include_directories: [swappy_inc], 74 | install: true, 75 | ) 76 | 77 | scdoc = find_program('scdoc', required: get_option('man-pages')) 78 | 79 | if scdoc.found() 80 | sh = find_program('sh') 81 | 82 | man_pages = ['swappy.1.scd'] 83 | 84 | mandir = get_option('mandir') 85 | 86 | foreach src : man_pages 87 | topic = src.split('.')[0] 88 | section = src.split('.')[1] 89 | output = '@0@.@1@'.format(topic, section) 90 | 91 | custom_target( 92 | output, 93 | input: src, 94 | output: output, 95 | command: [ 96 | sh, '-c', '@0@ < @INPUT@ > @1@'.format(scdoc.path(), output) 97 | ], 98 | install: true, 99 | install_dir: '@0@/man@1@'.format(mandir, section) 100 | ) 101 | endforeach 102 | endif 103 | -------------------------------------------------------------------------------- /meson_options.txt: -------------------------------------------------------------------------------- 1 | option('man-pages', type: 'feature', value: 'auto', description: 'Generate and install man pages') 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "swappy", 3 | "version": "1.8.0", 4 | "repository": { 5 | "type": "git", 6 | "url": "https://github.com/jtheoof/swappy.git" 7 | }, 8 | "author": "Jeremy Attali" 9 | } 10 | -------------------------------------------------------------------------------- /res/icons/hicolor/scalable/apps/swappy.svg: -------------------------------------------------------------------------------- 1 | 2 | 19 | 21 | 27 | 33 | 34 | 57 | 60 | 61 | 63 | 64 | 66 | image/svg+xml 67 | 69 | 70 | 71 | 72 | 73 | 77 | 84 | 89 | 94 | 95 | 113 | 117 | 118 | 119 | -------------------------------------------------------------------------------- /res/meson.build: -------------------------------------------------------------------------------- 1 | # Import the gnome module and use a GNOME function to ensure that application 2 | # resources will be compiled. 3 | gnome = import('gnome') 4 | 5 | # Icons 6 | install_subdir('icons', 7 | install_dir: join_paths(get_option('datadir')), 8 | ) 9 | 10 | swappy_resources = gnome.compile_resources('swappy', 11 | 'swappy.gresource.xml' 12 | ) 13 | -------------------------------------------------------------------------------- /res/style/swappy.css: -------------------------------------------------------------------------------- 1 | .drawing .text-button { 2 | font-family: "FontAwesome 5 Free Solid", "FontAwesome"; 3 | padding: 4px; 4 | } 5 | 6 | .drawing .text-button radio, 7 | .color-box .text-button radio { 8 | padding: 0; 9 | } 10 | 11 | .color-box button { 12 | padding: 6px 10px; 13 | } 14 | 15 | .color-box image { 16 | border-radius: 50px; 17 | } 18 | 19 | .color-box .color-red image { 20 | background-color: rgb(255, 0, 0); 21 | } 22 | 23 | .color-box .color-green image { 24 | background-color: rgb(0, 255, 0); 25 | } 26 | 27 | .color-box .color-blue image { 28 | background-color: rgb(0, 0, 255); 29 | } 30 | -------------------------------------------------------------------------------- /res/swappy.gresource.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | style/swappy.css 5 | swappy.glade 6 | 7 | 8 | -------------------------------------------------------------------------------- /script/bump-meson-build.js: -------------------------------------------------------------------------------- 1 | const projectVersionRegExp = /version: '(?\d+\.\d+\.\d+)',/; 2 | 3 | module.exports.readVersion = function (contents) { 4 | const matches = contents.match(projectVersionRegExp); 5 | 6 | return matches ? matches[1] : "unknown"; 7 | }; 8 | 9 | module.exports.writeVersion = function (_contents, version) { 10 | return _contents.replace(projectVersionRegExp, `version: '${version}',`); 11 | }; 12 | -------------------------------------------------------------------------------- /script/github-release: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | set -e 4 | 5 | declare -r git_root=$(git rev-parse --show-toplevel) 6 | declare -r app_name="swappy" 7 | declare -r release_folder="$git_root/release" 8 | 9 | declare version="" 10 | 11 | die() { 12 | echo "$*" 1>&2 13 | exit 1 14 | } 15 | 16 | init() { 17 | command -v git >/dev/null 2>&1 || { echo >&2 "git required: pacman -S git"; exit 1; } 18 | command -v gh >/dev/null 2>&1 || { echo >&2 "github cli tool required to publish the release: pacman -S github-cli"; exit 1; } 19 | command -v npx >/dev/null 2>&1 || { echo >&2 "npx required for standard versioning the release: pacman -S npm"; exit 1; } 20 | command -v gpg >/dev/null 2>&1 || { echo >&2 "gpg required to sign the archive: pacman -S gnupg"; exit 1; } 21 | 22 | mkdir -p $release_folder 23 | } 24 | 25 | git_get_release_version() { 26 | version=$(git describe --tags --abbrev=0 | sed 's/^v//') 27 | 28 | if [ -z "$version" ] 29 | then 30 | die "version not found, is the git tag valid?" 31 | fi 32 | 33 | echo "found latest version: $version" 34 | } 35 | 36 | npx_standard_version() { 37 | echo "setting up new standard version with npx..." 38 | npx standard-version --sign 39 | } 40 | 41 | git_push_tags() { 42 | echo "pushing git tags..." 43 | git push --follow-tags 44 | } 45 | 46 | 47 | git_build_archive() { 48 | echo "building source archives..." 49 | cd $git_root 50 | git archive -o "$release_folder/$app_name-$version.tar.gz" --format tar.gz --prefix "$app_name-$version/" "v$version" 51 | } 52 | 53 | download_source_for_release() { 54 | echo "downloading source assets..." 55 | cd $release_folder 56 | curl --location --output github-$app_name-$version.tar.gz https://github.com/jtheoof/$app_name/archive/v$version.tar.gz 57 | } 58 | 59 | verify_sha256_checksums() { 60 | echo "verifying signatures..." 61 | cd $release_folder 62 | sha256sum $app_name-$version.tar.gz | awk '{ print $1 }' > $app_name-$version.tar.gz.sha256 63 | 64 | # sha256sum --check will exit if the checksums do not match 65 | echo "$(cat $app_name-$version.tar.gz.sha256) github-$app_name-$version.tar.gz" | sha256sum --check 66 | } 67 | 68 | gpg_sign_archive() { 69 | echo "signing source assets..." 70 | cd $release_folder 71 | gpg --output $app_name-$version.tar.gz.sig --detach-sign $app_name-$version.tar.gz 72 | } 73 | 74 | git_generate_changelog() { 75 | echo "generating changelog..." 76 | git diff "v$version"^ -- CHANGELOG.md | tail -n +9 | head -n -4 | sed 's/^+//g' > $release_folder/CHANGELOG-$version.md 77 | } 78 | 79 | github_create_release() { 80 | echo "creating github release..." 81 | gh release create --draft "v$version" \ 82 | -F "$release_folder/CHANGELOG-$version.md" \ 83 | "$release_folder/$app_name-$version.tar.gz" \ 84 | "$release_folder/$app_name-$version.tar.gz.sig" \ 85 | "$release_folder/CHANGELOG-$version.md" 86 | } 87 | 88 | main() { 89 | init 90 | 91 | npx_standard_version 92 | git_push_tags 93 | git_get_release_version 94 | git_build_archive 95 | # Turning off manual downloading from github 96 | # doing all the steps, including archive, ourselves. 97 | #download_source_for_release 98 | #verify_sha256_checksums 99 | git_generate_changelog 100 | gpg_sign_archive 101 | github_create_release 102 | } 103 | 104 | main "$@" 105 | -------------------------------------------------------------------------------- /src/algebra.c: -------------------------------------------------------------------------------- 1 | #include "algebra.h" 2 | 3 | #include 4 | #include 5 | 6 | struct gaussian_kernel *gaussian_kernel(int width, double sigma) { 7 | double sum = 0; 8 | gint size = width * width + 1; 9 | double *kernel = g_new0(double, size); 10 | struct gaussian_kernel *gaussian = g_new(struct gaussian_kernel, 1); 11 | for (gint y = 0; y < width; y++) { 12 | for (gint x = 0; x < width; x++) { 13 | double j = y - width; 14 | double i = x - width; 15 | double cell = ((1.0 / (2.0 * G_PI * sigma)) * 16 | exp((-(i * i + j * j)) / (2.0 * sigma * sigma))) * 17 | 0xff; 18 | kernel[y * width + x] = cell; 19 | sum += cell; 20 | } 21 | } 22 | 23 | gaussian->kernel = kernel; 24 | gaussian->size = size; 25 | gaussian->sigma = sigma; 26 | gaussian->sum = sum; 27 | 28 | return gaussian; 29 | } 30 | 31 | void gaussian_kernel_free(gpointer data) { 32 | struct gaussian_kernel *gaussian = (struct gaussian_kernel *)data; 33 | if (gaussian != NULL) { 34 | g_free(gaussian->kernel); 35 | g_free(gaussian); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/application.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #include "clipboard.h" 10 | #include "config.h" 11 | #include "file.h" 12 | #include "paint.h" 13 | #include "pixbuf.h" 14 | #include "render.h" 15 | #include "swappy.h" 16 | 17 | static void update_ui_undo_redo(struct swappy_state *state) { 18 | GtkWidget *undo = GTK_WIDGET(state->ui->undo); 19 | GtkWidget *redo = GTK_WIDGET(state->ui->redo); 20 | gboolean undo_sensitive = g_list_length(state->paints) > 0; 21 | gboolean redo_sensitive = g_list_length(state->redo_paints) > 0; 22 | gtk_widget_set_sensitive(undo, undo_sensitive); 23 | gtk_widget_set_sensitive(redo, redo_sensitive); 24 | } 25 | 26 | static void update_ui_stroke_size_widget(struct swappy_state *state) { 27 | GtkButton *button = GTK_BUTTON(state->ui->line_size); 28 | char label[255]; 29 | g_snprintf(label, 255, "%.0lf", state->settings.w); 30 | gtk_button_set_label(button, label); 31 | } 32 | 33 | static void update_ui_text_size_widget(struct swappy_state *state) { 34 | GtkButton *button = GTK_BUTTON(state->ui->text_size); 35 | char label[255]; 36 | g_snprintf(label, 255, "%.0lf", state->settings.t); 37 | gtk_button_set_label(button, label); 38 | } 39 | 40 | static void update_ui_transparency_widget(struct swappy_state *state) { 41 | GtkButton *button = GTK_BUTTON(state->ui->transparency); 42 | char label[255]; 43 | g_snprintf(label, 255, "%" PRId32, state->settings.tr); 44 | gtk_button_set_label(button, label); 45 | } 46 | 47 | static void update_ui_panel_toggle_button(struct swappy_state *state) { 48 | GtkWidget *painting_box = GTK_WIDGET(state->ui->painting_box); 49 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(state->ui->panel_toggle_button); 50 | gboolean toggled = state->ui->panel_toggled; 51 | 52 | gtk_toggle_button_set_active(button, toggled); 53 | gtk_widget_set_visible(painting_box, toggled); 54 | } 55 | 56 | static void update_ui_fill_shape_toggle_button(struct swappy_state *state) { 57 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(state->ui->fill_shape); 58 | gboolean toggled = state->config->fill_shape; 59 | 60 | gtk_toggle_button_set_active(button, toggled); 61 | } 62 | 63 | static void update_ui_transparent_toggle_button(struct swappy_state *state) { 64 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(state->ui->transparent); 65 | gboolean toggled = state->config->transparent; 66 | 67 | gtk_toggle_button_set_active(button, toggled); 68 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->transparency), toggled); 69 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->transparency_minus), toggled); 70 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->transparency_plus), toggled); 71 | } 72 | 73 | void application_finish(struct swappy_state *state) { 74 | g_debug("application finishing, cleaning up"); 75 | paint_free_all(state); 76 | pixbuf_free(state); 77 | cairo_surface_destroy(state->rendering_surface); 78 | cairo_surface_destroy(state->original_image_surface); 79 | if (state->temp_file_str) { 80 | g_info("deleting temporary file: %s", state->temp_file_str); 81 | if (g_unlink(state->temp_file_str) != 0) { 82 | g_warning("unable to delete temporary file: %s", state->temp_file_str); 83 | } 84 | g_free(state->temp_file_str); 85 | } 86 | g_free(state->file_str); 87 | g_free(state->geometry); 88 | g_free(state->window); 89 | g_object_unref(state->ui->im_context); 90 | g_free(state->ui); 91 | 92 | g_object_unref(state->app); 93 | 94 | config_free(state); 95 | } 96 | 97 | static void action_undo(struct swappy_state *state) { 98 | GList *first = state->paints; 99 | 100 | if (first) { 101 | state->paints = g_list_remove_link(state->paints, first); 102 | state->redo_paints = g_list_prepend(state->redo_paints, first->data); 103 | 104 | render_state(state); 105 | update_ui_undo_redo(state); 106 | } 107 | } 108 | 109 | static void action_redo(struct swappy_state *state) { 110 | GList *first = state->redo_paints; 111 | 112 | if (first) { 113 | state->redo_paints = g_list_remove_link(state->redo_paints, first); 114 | state->paints = g_list_prepend(state->paints, first->data); 115 | 116 | render_state(state); 117 | update_ui_undo_redo(state); 118 | } 119 | } 120 | 121 | static void action_clear(struct swappy_state *state) { 122 | paint_free_all(state); 123 | render_state(state); 124 | update_ui_undo_redo(state); 125 | } 126 | 127 | static void action_toggle_painting_panel(struct swappy_state *state, 128 | gboolean *toggled) { 129 | state->ui->panel_toggled = 130 | (toggled == NULL) ? !state->ui->panel_toggled : *toggled; 131 | update_ui_panel_toggle_button(state); 132 | } 133 | 134 | static void action_update_color_state(struct swappy_state *state, double r, 135 | double g, double b, double a, 136 | gboolean custom) { 137 | state->settings.r = r; 138 | state->settings.g = g; 139 | state->settings.b = b; 140 | state->settings.a = a; 141 | 142 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->color), custom); 143 | } 144 | 145 | static void action_set_color_from_custom(struct swappy_state *state) { 146 | GdkRGBA color; 147 | gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(state->ui->color), &color); 148 | 149 | action_update_color_state(state, color.red, color.green, color.blue, 150 | color.alpha, true); 151 | } 152 | 153 | static void switch_mode_to_brush(struct swappy_state *state) { 154 | state->mode = SWAPPY_PAINT_MODE_BRUSH; 155 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 156 | } 157 | 158 | static void switch_mode_to_text(struct swappy_state *state) { 159 | state->mode = SWAPPY_PAINT_MODE_TEXT; 160 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 161 | } 162 | 163 | static void switch_mode_to_rectangle(struct swappy_state *state) { 164 | state->mode = SWAPPY_PAINT_MODE_RECTANGLE; 165 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true); 166 | } 167 | 168 | static void switch_mode_to_ellipse(struct swappy_state *state) { 169 | state->mode = SWAPPY_PAINT_MODE_ELLIPSE; 170 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true); 171 | } 172 | 173 | static void switch_mode_to_arrow(struct swappy_state *state) { 174 | state->mode = SWAPPY_PAINT_MODE_ARROW; 175 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 176 | } 177 | 178 | static void switch_mode_to_blur(struct swappy_state *state) { 179 | state->mode = SWAPPY_PAINT_MODE_BLUR; 180 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 181 | } 182 | 183 | static void action_stroke_size_decrease(struct swappy_state *state) { 184 | guint step = state->settings.w <= 10 ? 1 : 5; 185 | 186 | state->settings.w -= step; 187 | 188 | if (state->settings.w < SWAPPY_LINE_SIZE_MIN) { 189 | state->settings.w = SWAPPY_LINE_SIZE_MIN; 190 | } 191 | 192 | update_ui_stroke_size_widget(state); 193 | } 194 | 195 | static void action_stroke_size_reset(struct swappy_state *state) { 196 | state->settings.w = state->config->line_size; 197 | 198 | update_ui_stroke_size_widget(state); 199 | } 200 | 201 | static void action_stroke_size_increase(struct swappy_state *state) { 202 | guint step = state->settings.w >= 10 ? 5 : 1; 203 | state->settings.w += step; 204 | 205 | if (state->settings.w > SWAPPY_LINE_SIZE_MAX) { 206 | state->settings.w = SWAPPY_LINE_SIZE_MAX; 207 | } 208 | 209 | update_ui_stroke_size_widget(state); 210 | } 211 | 212 | static void action_text_size_decrease(struct swappy_state *state) { 213 | guint step = state->settings.t <= 20 ? 1 : 5; 214 | state->settings.t -= step; 215 | 216 | if (state->settings.t < SWAPPY_TEXT_SIZE_MIN) { 217 | state->settings.t = SWAPPY_TEXT_SIZE_MIN; 218 | } 219 | 220 | update_ui_text_size_widget(state); 221 | } 222 | static void action_text_size_reset(struct swappy_state *state) { 223 | state->settings.t = state->config->text_size; 224 | update_ui_text_size_widget(state); 225 | } 226 | static void action_text_size_increase(struct swappy_state *state) { 227 | guint step = state->settings.t >= 20 ? 5 : 1; 228 | state->settings.t += step; 229 | 230 | if (state->settings.t > SWAPPY_TEXT_SIZE_MAX) { 231 | state->settings.t = SWAPPY_TEXT_SIZE_MAX; 232 | } 233 | 234 | update_ui_text_size_widget(state); 235 | } 236 | 237 | static void action_transparency_decrease(struct swappy_state *state) { 238 | state->settings.tr -= 10; 239 | 240 | if (state->settings.tr < SWAPPY_TRANSPARENCY_MIN) { 241 | state->settings.tr = SWAPPY_TRANSPARENCY_MIN; 242 | } else { 243 | // ceil to 10 244 | state->settings.tr += 5; 245 | state->settings.tr /= 10; 246 | state->settings.tr *= 10; 247 | } 248 | 249 | update_ui_transparency_widget(state); 250 | } 251 | static void action_transparency_reset(struct swappy_state *state) { 252 | state->settings.tr = state->config->transparency; 253 | update_ui_transparency_widget(state); 254 | } 255 | static void action_transparency_increase(struct swappy_state *state) { 256 | state->settings.tr += 10; 257 | 258 | if (state->settings.tr > SWAPPY_TRANSPARENCY_MAX) { 259 | state->settings.tr = SWAPPY_TRANSPARENCY_MAX; 260 | } else { 261 | // floor to 10 262 | state->settings.tr /= 10; 263 | state->settings.tr *= 10; 264 | } 265 | 266 | update_ui_transparency_widget(state); 267 | } 268 | 269 | static void action_fill_shape_toggle(struct swappy_state *state, 270 | gboolean *toggled) { 271 | // Don't allow changing the state via a shortcut if the button can't be 272 | // clicked. 273 | if (!gtk_widget_get_sensitive(GTK_WIDGET(state->ui->fill_shape))) return; 274 | 275 | gboolean toggle = (toggled == NULL) ? !state->config->fill_shape : *toggled; 276 | state->config->fill_shape = toggle; 277 | 278 | update_ui_fill_shape_toggle_button(state); 279 | } 280 | 281 | static void action_transparent_toggle(struct swappy_state *state, 282 | gboolean *toggled) { 283 | gboolean toggle = (toggled == NULL) ? !state->config->transparent : *toggled; 284 | state->config->transparent = toggle; 285 | 286 | update_ui_transparent_toggle_button(state); 287 | } 288 | 289 | static void save_state_to_file_or_folder(struct swappy_state *state, 290 | char *file) { 291 | GdkPixbuf *pixbuf = pixbuf_get_from_state(state); 292 | 293 | if (file == NULL) { 294 | pixbuf_save_state_to_folder(pixbuf, state->config->save_dir, 295 | state->config->save_filename_format); 296 | } else { 297 | pixbuf_save_to_file(pixbuf, file); 298 | } 299 | 300 | g_object_unref(pixbuf); 301 | 302 | if (state->config->early_exit) { 303 | gtk_main_quit(); 304 | } 305 | } 306 | 307 | // We might need to save twice, once for auto_save config 308 | // and one for the output_file from -o CLI option. 309 | static void maybe_save_output_file(struct swappy_state *state) { 310 | if (state->config->auto_save) { 311 | save_state_to_file_or_folder(state, NULL); 312 | } 313 | 314 | if (state->output_file) { 315 | save_state_to_file_or_folder(state, state->output_file); 316 | } 317 | } 318 | 319 | static void screen_coordinates_to_image_coordinates(struct swappy_state *state, 320 | gdouble screen_x, 321 | gdouble screen_y, 322 | gdouble *image_x, 323 | gdouble *image_y) { 324 | gdouble x, y; 325 | 326 | gint w = gdk_pixbuf_get_width(state->original_image); 327 | gint h = gdk_pixbuf_get_height(state->original_image); 328 | 329 | // Clamp coordinates to original image properties to avoid side effects in 330 | // rendering pipeline 331 | x = CLAMP(screen_x / state->scaling_factor, 0, w); 332 | y = CLAMP(screen_y / state->scaling_factor, 0, h); 333 | 334 | *image_x = x; 335 | *image_y = y; 336 | } 337 | 338 | static void commit_state(struct swappy_state *state) { 339 | paint_commit_temporary(state); 340 | paint_free_list(&state->redo_paints); 341 | render_state(state); 342 | update_ui_undo_redo(state); 343 | } 344 | 345 | void on_destroy(GtkApplication *application, gpointer data) { 346 | struct swappy_state *state = (struct swappy_state *)data; 347 | maybe_save_output_file(state); 348 | } 349 | 350 | void brush_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 351 | switch_mode_to_brush(state); 352 | } 353 | 354 | void text_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 355 | switch_mode_to_text(state); 356 | } 357 | 358 | void rectangle_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 359 | switch_mode_to_rectangle(state); 360 | } 361 | 362 | void ellipse_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 363 | switch_mode_to_ellipse(state); 364 | } 365 | 366 | void arrow_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 367 | switch_mode_to_arrow(state); 368 | } 369 | 370 | void blur_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 371 | switch_mode_to_blur(state); 372 | } 373 | 374 | void save_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 375 | // Commit a potential paint (e.g. text being written) 376 | commit_state(state); 377 | save_state_to_file_or_folder(state, NULL); 378 | } 379 | 380 | void clear_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 381 | action_clear(state); 382 | } 383 | 384 | void copy_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 385 | // Commit a potential paint (e.g. text being written) 386 | commit_state(state); 387 | clipboard_copy_drawing_area_to_selection(state); 388 | } 389 | 390 | void control_modifier_changed(bool pressed, struct swappy_state *state) { 391 | if (state->temp_paint != NULL) { 392 | switch (state->temp_paint->type) { 393 | case SWAPPY_PAINT_MODE_ELLIPSE: 394 | case SWAPPY_PAINT_MODE_RECTANGLE: 395 | paint_update_temporary_shape( 396 | state, state->temp_paint->content.shape.to.x, 397 | state->temp_paint->content.shape.to.y, pressed); 398 | render_state(state); 399 | break; 400 | default: 401 | break; 402 | } 403 | } 404 | } 405 | 406 | static void im_context_commit(GtkIMContext *imc, gchar *str, 407 | gpointer user_data) { 408 | struct swappy_state *state = (struct swappy_state *)(user_data); 409 | if (state->temp_paint && state->mode == SWAPPY_PAINT_MODE_TEXT) { 410 | paint_update_temporary_str(state, str); 411 | render_state(state); 412 | return; 413 | } 414 | } 415 | 416 | static void clipboard_paste_selection(struct swappy_state *state) { 417 | GtkClipboard *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); 418 | gchar *text = gtk_clipboard_wait_for_text(clipboard); 419 | if (text) { 420 | paint_update_temporary_str(state, text); 421 | g_free(text); 422 | } 423 | } 424 | 425 | void window_keypress_handler(GtkWidget *widget, GdkEventKey *event, 426 | struct swappy_state *state) { 427 | if (state->temp_paint && state->mode == SWAPPY_PAINT_MODE_TEXT) { 428 | /* ctrl-v: paste */ 429 | if (event->state & GDK_CONTROL_MASK && event->keyval == GDK_KEY_v) { 430 | clipboard_paste_selection(state); 431 | } else { 432 | paint_update_temporary_text(state, event); 433 | } 434 | render_state(state); 435 | return; 436 | } 437 | if (event->state & GDK_CONTROL_MASK) { 438 | switch (event->keyval) { 439 | case GDK_KEY_c: 440 | clipboard_copy_drawing_area_to_selection(state); 441 | break; 442 | case GDK_KEY_s: 443 | save_state_to_file_or_folder(state, NULL); 444 | break; 445 | case GDK_KEY_b: 446 | action_toggle_painting_panel(state, NULL); 447 | break; 448 | case GDK_KEY_w: 449 | gtk_main_quit(); 450 | break; 451 | case GDK_KEY_z: 452 | action_undo(state); 453 | break; 454 | case GDK_KEY_Z: 455 | case GDK_KEY_y: 456 | action_redo(state); 457 | break; 458 | default: 459 | break; 460 | } 461 | } else { 462 | switch (event->keyval) { 463 | case GDK_KEY_Escape: 464 | case GDK_KEY_q: 465 | maybe_save_output_file(state); 466 | gtk_main_quit(); 467 | break; 468 | case GDK_KEY_b: 469 | switch_mode_to_brush(state); 470 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->brush), true); 471 | break; 472 | case GDK_KEY_e: 473 | case GDK_KEY_t: 474 | switch_mode_to_text(state); 475 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->text), true); 476 | break; 477 | case GDK_KEY_s: 478 | case GDK_KEY_r: 479 | switch_mode_to_rectangle(state); 480 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->rectangle), 481 | true); 482 | break; 483 | case GDK_KEY_c: 484 | case GDK_KEY_o: 485 | switch_mode_to_ellipse(state); 486 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->ellipse), 487 | true); 488 | break; 489 | case GDK_KEY_a: 490 | switch_mode_to_arrow(state); 491 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->arrow), true); 492 | break; 493 | case GDK_KEY_d: 494 | switch_mode_to_blur(state); 495 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blur), true); 496 | break; 497 | case GDK_KEY_x: 498 | case GDK_KEY_k: 499 | action_clear(state); 500 | break; 501 | case GDK_KEY_R: 502 | action_update_color_state(state, 1, 0, 0, 1, false); 503 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->red), true); 504 | break; 505 | case GDK_KEY_G: 506 | action_update_color_state(state, 0, 1, 0, 1, false); 507 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->green), true); 508 | break; 509 | case GDK_KEY_B: 510 | action_update_color_state(state, 0, 0, 1, 1, false); 511 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blue), true); 512 | break; 513 | case GDK_KEY_C: 514 | action_set_color_from_custom(state); 515 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->custom), 516 | true); 517 | break; 518 | case GDK_KEY_minus: 519 | action_stroke_size_decrease(state); 520 | break; 521 | case GDK_KEY_equal: 522 | action_stroke_size_reset(state); 523 | break; 524 | case GDK_KEY_plus: 525 | action_stroke_size_increase(state); 526 | break; 527 | case GDK_KEY_Control_L: 528 | control_modifier_changed(true, state); 529 | break; 530 | case GDK_KEY_f: 531 | action_fill_shape_toggle(state, NULL); 532 | break; 533 | case GDK_KEY_T: 534 | action_transparent_toggle(state, NULL); 535 | break; 536 | default: 537 | break; 538 | } 539 | } 540 | } 541 | 542 | void window_keyrelease_handler(GtkWidget *widget, GdkEventKey *event, 543 | struct swappy_state *state) { 544 | if (event->state & GDK_CONTROL_MASK) { 545 | switch (event->keyval) { 546 | case GDK_KEY_Control_L: 547 | control_modifier_changed(false, state); 548 | break; 549 | default: 550 | break; 551 | } 552 | } else { 553 | switch (event->keyval) { 554 | default: 555 | break; 556 | } 557 | } 558 | } 559 | 560 | gboolean window_delete_handler(GtkWidget *widget, GdkEvent *event, 561 | struct swappy_state *state) { 562 | gtk_main_quit(); 563 | return FALSE; 564 | } 565 | 566 | void pane_toggled_handler(GtkWidget *widget, struct swappy_state *state) { 567 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(widget); 568 | gboolean toggled = gtk_toggle_button_get_active(button); 569 | action_toggle_painting_panel(state, &toggled); 570 | } 571 | 572 | void undo_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 573 | action_undo(state); 574 | } 575 | 576 | void redo_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 577 | action_redo(state); 578 | } 579 | 580 | gboolean draw_area_handler(GtkWidget *widget, cairo_t *cr, 581 | struct swappy_state *state) { 582 | GtkAllocation *alloc = g_new(GtkAllocation, 1); 583 | gtk_widget_get_allocation(widget, alloc); 584 | 585 | GdkPixbuf *image = state->original_image; 586 | gint image_width = gdk_pixbuf_get_width(image); 587 | gint image_height = gdk_pixbuf_get_height(image); 588 | double scale_x = (double)alloc->width / image_width; 589 | double scale_y = (double)alloc->height / image_height; 590 | 591 | cairo_scale(cr, scale_x, scale_y); 592 | cairo_set_source_surface(cr, state->rendering_surface, 0, 0); 593 | cairo_paint(cr); 594 | 595 | return FALSE; 596 | } 597 | 598 | gboolean draw_area_configure_handler(GtkWidget *widget, 599 | GdkEventConfigure *event, 600 | struct swappy_state *state) { 601 | g_debug("received configure_event callback"); 602 | 603 | pixbuf_scale_surface_from_widget(state, widget); 604 | 605 | render_state(state); 606 | 607 | return TRUE; 608 | } 609 | 610 | void draw_area_button_press_handler(GtkWidget *widget, GdkEventButton *event, 611 | struct swappy_state *state) { 612 | gdouble x, y; 613 | 614 | screen_coordinates_to_image_coordinates(state, event->x, event->y, &x, &y); 615 | 616 | if (event->button == 1) { 617 | switch (state->mode) { 618 | case SWAPPY_PAINT_MODE_BLUR: 619 | case SWAPPY_PAINT_MODE_BRUSH: 620 | case SWAPPY_PAINT_MODE_RECTANGLE: 621 | case SWAPPY_PAINT_MODE_ELLIPSE: 622 | case SWAPPY_PAINT_MODE_ARROW: 623 | case SWAPPY_PAINT_MODE_TEXT: 624 | paint_add_temporary(state, x, y, state->mode); 625 | render_state(state); 626 | update_ui_undo_redo(state); 627 | break; 628 | default: 629 | return; 630 | } 631 | } 632 | } 633 | void draw_area_motion_notify_handler(GtkWidget *widget, GdkEventMotion *event, 634 | struct swappy_state *state) { 635 | gdouble x, y; 636 | 637 | screen_coordinates_to_image_coordinates(state, event->x, event->y, &x, &y); 638 | 639 | GdkDisplay *display = gdk_display_get_default(); 640 | GdkWindow *window = event->window; 641 | GdkCursor *crosshair = gdk_cursor_new_for_display(display, GDK_CROSSHAIR); 642 | gdk_window_set_cursor(window, crosshair); 643 | 644 | gboolean is_button1_pressed = event->state & GDK_BUTTON1_MASK; 645 | gboolean is_control_pressed = event->state & GDK_CONTROL_MASK; 646 | 647 | switch (state->mode) { 648 | case SWAPPY_PAINT_MODE_BLUR: 649 | case SWAPPY_PAINT_MODE_BRUSH: 650 | case SWAPPY_PAINT_MODE_RECTANGLE: 651 | case SWAPPY_PAINT_MODE_ELLIPSE: 652 | case SWAPPY_PAINT_MODE_ARROW: 653 | if (is_button1_pressed) { 654 | paint_update_temporary_shape(state, x, y, is_control_pressed); 655 | render_state(state); 656 | } 657 | break; 658 | case SWAPPY_PAINT_MODE_TEXT: 659 | if (is_button1_pressed) { 660 | paint_update_temporary_text_clip(state, x, y); 661 | render_state(state); 662 | } 663 | break; 664 | default: 665 | return; 666 | } 667 | g_object_unref(crosshair); 668 | } 669 | void draw_area_button_release_handler(GtkWidget *widget, GdkEventButton *event, 670 | struct swappy_state *state) { 671 | if (!(event->state & GDK_BUTTON1_MASK)) { 672 | return; 673 | } 674 | 675 | switch (state->mode) { 676 | case SWAPPY_PAINT_MODE_BLUR: 677 | case SWAPPY_PAINT_MODE_BRUSH: 678 | case SWAPPY_PAINT_MODE_RECTANGLE: 679 | case SWAPPY_PAINT_MODE_ELLIPSE: 680 | case SWAPPY_PAINT_MODE_ARROW: 681 | commit_state(state); 682 | break; 683 | case SWAPPY_PAINT_MODE_TEXT: 684 | if (state->temp_paint && !state->temp_paint->can_draw) { 685 | paint_free(state->temp_paint); 686 | state->temp_paint = NULL; 687 | } 688 | break; 689 | default: 690 | return; 691 | } 692 | } 693 | 694 | void color_red_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 695 | action_update_color_state(state, 1, 0, 0, 1, false); 696 | } 697 | 698 | void color_green_clicked_handler(GtkWidget *widget, 699 | struct swappy_state *state) { 700 | action_update_color_state(state, 0, 1, 0, 1, false); 701 | } 702 | 703 | void color_blue_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 704 | action_update_color_state(state, 0, 0, 1, 1, false); 705 | } 706 | 707 | void color_custom_clicked_handler(GtkWidget *widget, 708 | struct swappy_state *state) { 709 | action_set_color_from_custom(state); 710 | } 711 | 712 | void color_custom_color_set_handler(GtkWidget *widget, 713 | struct swappy_state *state) { 714 | action_set_color_from_custom(state); 715 | } 716 | 717 | void stroke_size_decrease_handler(GtkWidget *widget, 718 | struct swappy_state *state) { 719 | action_stroke_size_decrease(state); 720 | } 721 | 722 | void stroke_size_reset_handler(GtkWidget *widget, struct swappy_state *state) { 723 | action_stroke_size_reset(state); 724 | } 725 | void stroke_size_increase_handler(GtkWidget *widget, 726 | struct swappy_state *state) { 727 | action_stroke_size_increase(state); 728 | } 729 | 730 | void text_size_decrease_handler(GtkWidget *widget, struct swappy_state *state) { 731 | action_text_size_decrease(state); 732 | } 733 | void text_size_reset_handler(GtkWidget *widget, struct swappy_state *state) { 734 | action_text_size_reset(state); 735 | } 736 | void text_size_increase_handler(GtkWidget *widget, struct swappy_state *state) { 737 | action_text_size_increase(state); 738 | } 739 | 740 | void transparency_decrease_handler(GtkWidget *widget, 741 | struct swappy_state *state) { 742 | action_transparency_decrease(state); 743 | } 744 | void transparency_reset_handler(GtkWidget *widget, struct swappy_state *state) { 745 | action_transparency_reset(state); 746 | } 747 | void transparency_increase_handler(GtkWidget *widget, 748 | struct swappy_state *state) { 749 | action_transparency_increase(state); 750 | } 751 | 752 | void fill_shape_toggled_handler(GtkWidget *widget, struct swappy_state *state) { 753 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(widget); 754 | gboolean toggled = gtk_toggle_button_get_active(button); 755 | action_fill_shape_toggle(state, &toggled); 756 | } 757 | 758 | void transparent_toggled_handler(GtkWidget *widget, 759 | struct swappy_state *state) { 760 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(widget); 761 | gboolean toggled = gtk_toggle_button_get_active(button); 762 | action_transparent_toggle(state, &toggled); 763 | } 764 | 765 | static void compute_window_size_and_scaling_factor(struct swappy_state *state) { 766 | GdkRectangle workarea = {0}; 767 | GdkDisplay *display = gdk_display_get_default(); 768 | GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(state->ui->window)); 769 | GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window); 770 | gdk_monitor_get_workarea(monitor, &workarea); 771 | 772 | g_assert(workarea.width > 0); 773 | g_assert(workarea.height > 0); 774 | 775 | if (state->window) { 776 | g_free(state->window); 777 | state->window = NULL; 778 | } 779 | 780 | state->window = g_new(struct swappy_box, 1); 781 | state->window->x = workarea.x; 782 | state->window->y = workarea.y; 783 | 784 | double threshold = 0.75; 785 | double scaling_factor = 1.0; 786 | 787 | int image_width = gdk_pixbuf_get_width(state->original_image); 788 | int image_height = gdk_pixbuf_get_height(state->original_image); 789 | 790 | int max_width = workarea.width * threshold; 791 | int max_height = workarea.height * threshold; 792 | 793 | g_info("size of image: %ux%u", image_width, image_height); 794 | g_info("size of monitor at window: %ux%u", workarea.width, workarea.height); 795 | g_info("maxium size allowed for window: %ux%u", max_width, max_height); 796 | 797 | int scaled_width = image_width; 798 | int scaled_height = image_height; 799 | 800 | double scaling_factor_width = (double)max_width / image_width; 801 | double scaling_factor_height = (double)max_height / image_height; 802 | 803 | if (scaling_factor_height < 1.0 || scaling_factor_width < 1.0) { 804 | scaling_factor = MIN(scaling_factor_width, scaling_factor_height); 805 | scaled_width = image_width * scaling_factor; 806 | scaled_height = image_height * scaling_factor; 807 | g_info("rendering area will be scaled by a factor of: %.2lf", 808 | scaling_factor); 809 | } 810 | 811 | state->scaling_factor = scaling_factor; 812 | state->window->width = scaled_width; 813 | state->window->height = scaled_height; 814 | 815 | g_info("size of window to render: %ux%u", state->window->width, 816 | state->window->height); 817 | } 818 | 819 | static void apply_css(GtkWidget *widget, GtkStyleProvider *provider) { 820 | gtk_style_context_add_provider(gtk_widget_get_style_context(widget), provider, 821 | GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); 822 | if (GTK_IS_CONTAINER(widget)) { 823 | gtk_container_forall(GTK_CONTAINER(widget), (GtkCallback)apply_css, 824 | provider); 825 | } 826 | } 827 | 828 | static bool load_css(struct swappy_state *state) { 829 | GtkCssProvider *provider = gtk_css_provider_new(); 830 | gtk_css_provider_load_from_resource(provider, 831 | "/me/jtheoof/swappy/style/swappy.css"); 832 | apply_css(GTK_WIDGET(state->ui->window), GTK_STYLE_PROVIDER(provider)); 833 | g_object_unref(provider); 834 | return true; 835 | } 836 | 837 | static bool load_layout(struct swappy_state *state) { 838 | GError *error = NULL; 839 | // init color 840 | GdkRGBA color; 841 | 842 | /* Construct a GtkBuilder instance and load our UI description */ 843 | GtkBuilder *builder = gtk_builder_new(); 844 | 845 | // Set translation domain for the application based on `src/po/meson.build` 846 | gtk_builder_set_translation_domain(builder, GETTEXT_PACKAGE); 847 | 848 | if (gtk_builder_add_from_resource(builder, "/me/jtheoof/swappy/swappy.glade", 849 | &error) == 0) { 850 | g_printerr("Error loading file: %s", error->message); 851 | g_clear_error(&error); 852 | return false; 853 | } 854 | 855 | gtk_builder_connect_signals(builder, state); 856 | 857 | GtkWindow *window = 858 | GTK_WINDOW(gtk_builder_get_object(builder, "paint-window")); 859 | GtkIMContext *im_context = gtk_im_multicontext_new(); 860 | gtk_im_context_set_client_window(im_context, 861 | gtk_widget_get_window(GTK_WIDGET(window))); 862 | g_signal_connect(G_OBJECT(im_context), "commit", 863 | G_CALLBACK(im_context_commit), state); 864 | state->ui->im_context = im_context; 865 | 866 | g_signal_connect(window, "destroy", G_CALLBACK(on_destroy), state); 867 | 868 | state->ui->panel_toggle_button = 869 | GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "btn-toggle-panel")); 870 | 871 | state->ui->undo = GTK_BUTTON(gtk_builder_get_object(builder, "undo-button")); 872 | state->ui->redo = GTK_BUTTON(gtk_builder_get_object(builder, "redo-button")); 873 | 874 | GtkWidget *area = 875 | GTK_WIDGET(gtk_builder_get_object(builder, "painting-area")); 876 | 877 | state->ui->painting_box = 878 | GTK_BOX(gtk_builder_get_object(builder, "painting-box")); 879 | GtkRadioButton *brush = 880 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "brush")); 881 | GtkRadioButton *text = 882 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "text")); 883 | GtkRadioButton *rectangle = 884 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "rectangle")); 885 | GtkRadioButton *ellipse = 886 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "ellipse")); 887 | GtkRadioButton *arrow = 888 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "arrow")); 889 | GtkRadioButton *blur = 890 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "blur")); 891 | 892 | state->ui->red = 893 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-red-button")); 894 | state->ui->green = 895 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-green-button")); 896 | state->ui->blue = 897 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-blue-button")); 898 | state->ui->custom = 899 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-custom-button")); 900 | state->ui->color = 901 | GTK_COLOR_BUTTON(gtk_builder_get_object(builder, "custom-color-button")); 902 | 903 | state->ui->line_size = 904 | GTK_BUTTON(gtk_builder_get_object(builder, "stroke-size-button")); 905 | state->ui->text_size = 906 | GTK_BUTTON(gtk_builder_get_object(builder, "text-size-button")); 907 | state->ui->transparency = 908 | GTK_BUTTON(gtk_builder_get_object(builder, "transparency-button")); 909 | state->ui->transparency_plus = 910 | GTK_BUTTON(gtk_builder_get_object(builder, "transparency-plus-button")); 911 | state->ui->transparency_minus = 912 | GTK_BUTTON(gtk_builder_get_object(builder, "transparency-minus-button")); 913 | 914 | state->ui->fill_shape = GTK_TOGGLE_BUTTON( 915 | gtk_builder_get_object(builder, "fill-shape-toggle-button")); 916 | 917 | gdk_rgba_parse(&color, state->config->custom_color); 918 | gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(state->ui->color), &color); 919 | state->ui->transparent = GTK_TOGGLE_BUTTON( 920 | gtk_builder_get_object(builder, "transparent-toggle-button")); 921 | 922 | state->ui->brush = brush; 923 | state->ui->text = text; 924 | state->ui->rectangle = rectangle; 925 | state->ui->ellipse = ellipse; 926 | state->ui->arrow = arrow; 927 | state->ui->blur = blur; 928 | state->ui->area = area; 929 | state->ui->window = window; 930 | 931 | compute_window_size_and_scaling_factor(state); 932 | gtk_widget_set_size_request(area, state->window->width, 933 | state->window->height); 934 | action_toggle_painting_panel(state, &state->config->show_panel); 935 | 936 | g_object_unref(G_OBJECT(builder)); 937 | 938 | return true; 939 | } 940 | 941 | static void set_paint_mode(struct swappy_state *state) { 942 | switch (state->mode) { 943 | case SWAPPY_PAINT_MODE_BRUSH: 944 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->brush), true); 945 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 946 | break; 947 | case SWAPPY_PAINT_MODE_TEXT: 948 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->text), true); 949 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 950 | break; 951 | case SWAPPY_PAINT_MODE_RECTANGLE: 952 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->rectangle), 953 | true); 954 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true); 955 | break; 956 | case SWAPPY_PAINT_MODE_ELLIPSE: 957 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->ellipse), true); 958 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true); 959 | break; 960 | case SWAPPY_PAINT_MODE_ARROW: 961 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->arrow), true); 962 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 963 | break; 964 | case SWAPPY_PAINT_MODE_BLUR: 965 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blur), true); 966 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 967 | break; 968 | default: 969 | break; 970 | } 971 | } 972 | 973 | static bool init_gtk_window(struct swappy_state *state) { 974 | if (!state->original_image) { 975 | g_critical("original image not loaded"); 976 | return false; 977 | } 978 | 979 | if (!load_layout(state)) { 980 | return false; 981 | } 982 | 983 | if (!load_css(state)) { 984 | return false; 985 | } 986 | 987 | set_paint_mode(state); 988 | 989 | update_ui_stroke_size_widget(state); 990 | update_ui_text_size_widget(state); 991 | update_ui_transparency_widget(state); 992 | update_ui_undo_redo(state); 993 | update_ui_panel_toggle_button(state); 994 | update_ui_fill_shape_toggle_button(state); 995 | update_ui_transparent_toggle_button(state); 996 | 997 | return true; 998 | } 999 | 1000 | static gboolean has_option_file(struct swappy_state *state) { 1001 | return (state->file_str != NULL); 1002 | } 1003 | 1004 | static gboolean is_file_from_stdin(const char *file) { 1005 | return (strcmp(file, "-") == 0); 1006 | } 1007 | 1008 | static void init_settings(struct swappy_state *state) { 1009 | state->settings.r = 1; 1010 | state->settings.g = 0; 1011 | state->settings.b = 0; 1012 | state->settings.a = 1; 1013 | state->settings.w = state->config->line_size; 1014 | state->settings.t = state->config->text_size; 1015 | state->settings.tr = state->config->transparency; 1016 | state->mode = state->config->paint_mode; 1017 | } 1018 | 1019 | static gint command_line_handler(GtkApplication *app, 1020 | GApplicationCommandLine *cmdline, 1021 | struct swappy_state *state) { 1022 | config_load(state); 1023 | init_settings(state); 1024 | 1025 | if (has_option_file(state)) { 1026 | if (is_file_from_stdin(state->file_str)) { 1027 | char *temp_file_str = file_dump_stdin_into_a_temp_file(); 1028 | state->temp_file_str = temp_file_str; 1029 | } 1030 | 1031 | if (!pixbuf_init_from_file(state)) { 1032 | return EXIT_FAILURE; 1033 | } 1034 | } 1035 | 1036 | if (!init_gtk_window(state)) { 1037 | return EXIT_FAILURE; 1038 | } 1039 | 1040 | return EXIT_SUCCESS; 1041 | } 1042 | 1043 | // Print version and quit 1044 | gboolean callback_on_flag(const gchar *option_name, const gchar *value, 1045 | gpointer data, GError **error) { 1046 | if (!strcmp(option_name, "-v") || !strcmp(option_name, "--version")) { 1047 | printf("swappy version %s\n", SWAPPY_VERSION); 1048 | exit(0); 1049 | } 1050 | return TRUE; 1051 | } 1052 | 1053 | bool application_init(struct swappy_state *state) { 1054 | // Callback function for flags 1055 | gboolean (*GOptionArgFunc)(const gchar *option_name, const gchar *value, 1056 | gpointer data, GError **error); 1057 | GOptionArgFunc = &callback_on_flag; 1058 | 1059 | const GOptionEntry cli_options[] = { 1060 | { 1061 | .long_name = "file", 1062 | .short_name = 'f', 1063 | .arg = G_OPTION_ARG_STRING, 1064 | .arg_data = &state->file_str, 1065 | .description = "Load a file at a specific path", 1066 | }, 1067 | { 1068 | .long_name = "output-file", 1069 | .short_name = 'o', 1070 | .arg = G_OPTION_ARG_STRING, 1071 | .arg_data = &state->output_file, 1072 | .description = "Print the final surface to the given file when " 1073 | "exiting, use - to print to stdout", 1074 | }, 1075 | { 1076 | .long_name = "version", 1077 | .short_name = 'v', 1078 | .flags = G_OPTION_FLAG_NO_ARG, 1079 | .arg = G_OPTION_ARG_CALLBACK, 1080 | .arg_data = GOptionArgFunc, 1081 | .description = "Print version and quit", 1082 | }, 1083 | {NULL}}; // NOLINT(clang-diagnostic-missing-field-initializers) 1084 | 1085 | state->app = gtk_application_new("me.jtheoof.swappy", 1086 | G_APPLICATION_HANDLES_COMMAND_LINE); 1087 | 1088 | if (state->app == NULL) { 1089 | g_critical("cannot create gtk application"); 1090 | return false; 1091 | } 1092 | 1093 | g_application_add_main_option_entries(G_APPLICATION(state->app), cli_options); 1094 | 1095 | state->ui = g_new(struct swappy_state_ui, 1); 1096 | state->ui->panel_toggled = false; 1097 | 1098 | g_signal_connect(state->app, "command-line", G_CALLBACK(command_line_handler), 1099 | state); 1100 | 1101 | return true; 1102 | } 1103 | 1104 | int application_run(struct swappy_state *state) { 1105 | return g_application_run(G_APPLICATION(state->app), state->argc, state->argv); 1106 | } 1107 | -------------------------------------------------------------------------------- /src/box.c: -------------------------------------------------------------------------------- 1 | #include "box.h" 2 | 3 | static int32_t lmax(int32_t a, int32_t b) { return a > b ? a : b; } 4 | 5 | static int32_t lmin(int32_t a, int32_t b) { return a < b ? a : b; } 6 | 7 | bool box_parse(struct swappy_box *box, const char *str) { 8 | char *end = NULL; 9 | box->x = (int32_t)strtol(str, &end, 10); 10 | if (end[0] != ',') { 11 | return false; 12 | } 13 | 14 | char *next = end + 1; 15 | box->y = (int32_t)strtol(next, &end, 10); 16 | if (end[0] != ' ') { 17 | return false; 18 | } 19 | 20 | next = end + 1; 21 | box->width = (int32_t)strtol(next, &end, 10); 22 | if (end[0] != 'x') { 23 | return false; 24 | } 25 | 26 | next = end + 1; 27 | box->height = (int32_t)strtol(next, &end, 10); 28 | if (end[0] != '\0') { 29 | return false; 30 | } 31 | 32 | return true; 33 | } 34 | 35 | bool is_empty_box(struct swappy_box *box) { 36 | return box->width <= 0 || box->height <= 0; 37 | } 38 | 39 | bool intersect_box(struct swappy_box *a, struct swappy_box *b) { 40 | if (is_empty_box(a) || is_empty_box(b)) { 41 | return false; 42 | } 43 | 44 | int32_t x1 = lmax(a->x, b->x); 45 | int32_t y1 = lmax(a->y, b->y); 46 | int32_t x2 = lmin(a->x + a->width, b->x + b->width); 47 | int32_t y2 = lmin(a->y + a->height, b->y + b->height); 48 | 49 | struct swappy_box box = { 50 | .x = x1, 51 | .y = y1, 52 | .width = x2 - x1, 53 | .height = y2 - y1, 54 | }; 55 | return !is_empty_box(&box); 56 | } 57 | -------------------------------------------------------------------------------- /src/clipboard.c: -------------------------------------------------------------------------------- 1 | #include "clipboard.h" 2 | 3 | #include 4 | #include 5 | #include 6 | 7 | #include "pixbuf.h" 8 | #include "util.h" 9 | 10 | #define gtk_clipboard_t GtkClipboard 11 | #define gdk_pixbuf_t GdkPixbuf 12 | 13 | static gboolean send_pixbuf_to_wl_copy(gdk_pixbuf_t *pixbuf) { 14 | pid_t clipboard_process = 0; 15 | int pipefd[2]; 16 | int status; 17 | ssize_t written; 18 | gsize size; 19 | gchar *buffer = NULL; 20 | GError *error = NULL; 21 | 22 | if (pipe(pipefd) < 0) { 23 | g_warning("unable to pipe for copy process to work"); 24 | return false; 25 | } 26 | clipboard_process = fork(); 27 | if (clipboard_process == -1) { 28 | g_warning("unable to fork process for copy"); 29 | return false; 30 | } 31 | if (clipboard_process == 0) { 32 | close(pipefd[1]); 33 | dup2(pipefd[0], STDIN_FILENO); 34 | close(pipefd[0]); 35 | execlp("wl-copy", "wl-copy", "-t", "image/png", NULL); 36 | g_warning( 37 | "Unable to copy contents to clipboard. Please make sure you have " 38 | "`wl-clipboard`, `xclip`, or `xsel` installed."); 39 | exit(1); 40 | } 41 | close(pipefd[0]); 42 | 43 | gdk_pixbuf_save_to_buffer(pixbuf, &buffer, &size, "png", &error, NULL); 44 | 45 | if (error != NULL) { 46 | g_critical("unable to save pixbuf to buffer for copy: %s", error->message); 47 | g_error_free(error); 48 | return false; 49 | } 50 | 51 | written = write(pipefd[1], buffer, size); 52 | if (written == -1) { 53 | g_warning("unable to write to pipe fd for copy"); 54 | g_free(buffer); 55 | return false; 56 | } 57 | 58 | close(pipefd[1]); 59 | g_free(buffer); 60 | waitpid(clipboard_process, &status, 0); 61 | 62 | if (WIFEXITED(status)) { 63 | return WEXITSTATUS(status) == 0; // Make sure the child exited properly 64 | } 65 | 66 | return false; 67 | } 68 | 69 | static void send_pixbuf_to_gdk_clipboard(gdk_pixbuf_t *pixbuf) { 70 | gtk_clipboard_t *clipboard = gtk_clipboard_get(GDK_SELECTION_CLIPBOARD); 71 | gtk_clipboard_set_image(clipboard, pixbuf); 72 | gtk_clipboard_store(clipboard); // Does not work for Wayland gdk backend 73 | } 74 | 75 | bool clipboard_copy_drawing_area_to_selection(struct swappy_state *state) { 76 | gdk_pixbuf_t *pixbuf = pixbuf_get_from_state(state); 77 | 78 | // Try `wl-copy` first and fall back to gtk function. See README.md. 79 | if (!send_pixbuf_to_wl_copy(pixbuf)) { 80 | send_pixbuf_to_gdk_clipboard(pixbuf); 81 | } 82 | 83 | g_object_unref(pixbuf); 84 | 85 | if (state->config->early_exit) { 86 | gtk_main_quit(); 87 | } 88 | 89 | return true; 90 | } 91 | -------------------------------------------------------------------------------- /src/config.c: -------------------------------------------------------------------------------- 1 | #include "config.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include "file.h" 11 | #include "swappy.h" 12 | 13 | static void print_config(struct swappy_config *config) { 14 | g_info("printing config:"); 15 | g_info("config_dir: %s", config->config_file); 16 | g_info("save_dir: %s", config->save_dir); 17 | g_info("save_filename_format: %s", config->save_filename_format); 18 | g_info("show_panel: %d", config->show_panel); 19 | g_info("line_size: %d", config->line_size); 20 | g_info("text_font: %s", config->text_font); 21 | g_info("text_size: %d", config->text_size); 22 | g_info("transparency: %d", config->transparency); 23 | g_info("paint_mode: %d", config->paint_mode); 24 | g_info("early_exit: %d", config->early_exit); 25 | g_info("fill_shape: %d", config->fill_shape); 26 | g_info("auto_save: %d", config->auto_save); 27 | g_info("custom_color: %s", config->custom_color); 28 | g_info("transparent: %d", config->transparent); 29 | } 30 | 31 | static char *get_default_save_dir() { 32 | static const char *storage_paths[] = { 33 | "$XDG_DESKTOP_DIR", 34 | "$XDG_CONFIG_HOME/Desktop", 35 | "$HOME/Desktop", 36 | "$HOME", 37 | }; 38 | 39 | for (size_t i = 0; i < sizeof(storage_paths) / sizeof(char *); ++i) { 40 | wordexp_t p; 41 | if (wordexp(storage_paths[i], &p, 0) == 0) { 42 | char *path = g_strdup(p.we_wordv[0]); 43 | wordfree(&p); 44 | if (path && folder_exists(path)) { 45 | return path; 46 | } 47 | g_free(path); 48 | } 49 | } 50 | 51 | return NULL; 52 | } 53 | 54 | static char *get_config_file() { 55 | static const char *storage_paths[] = { 56 | "$XDG_CONFIG_HOME/swappy/config", 57 | "$HOME/.config/swappy/config", 58 | }; 59 | 60 | for (size_t i = 0; i < sizeof(storage_paths) / sizeof(char *); ++i) { 61 | wordexp_t p; 62 | if (wordexp(storage_paths[i], &p, 0) == 0) { 63 | char *path = g_strdup(p.we_wordv[0]); 64 | wordfree(&p); 65 | if (path && file_exists(path)) { 66 | return path; 67 | } 68 | g_free(path); 69 | } 70 | } 71 | 72 | return NULL; 73 | } 74 | 75 | static void load_config_from_file(struct swappy_config *config, 76 | const char *file) { 77 | GKeyFile *gkf; 78 | const gchar *group = "Default"; 79 | gchar *save_dir = NULL; 80 | gchar *save_filename_format = NULL; 81 | gboolean show_panel; 82 | gchar *save_dir_expanded = NULL; 83 | guint64 line_size, text_size; 84 | guint64 transparency; 85 | gchar *text_font = NULL; 86 | gchar *paint_mode = NULL; 87 | gboolean early_exit; 88 | gboolean fill_shape; 89 | gboolean auto_save; 90 | gchar *custom_color = NULL; 91 | gboolean transparent; 92 | GError *error = NULL; 93 | 94 | if (file == NULL) { 95 | return; 96 | } 97 | 98 | gkf = g_key_file_new(); 99 | 100 | if (!g_key_file_load_from_file(gkf, file, G_KEY_FILE_NONE, NULL)) { 101 | g_warning("could not read config file %s", file); 102 | g_key_file_free(gkf); 103 | return; 104 | } 105 | 106 | save_dir = g_key_file_get_string(gkf, group, "save_dir", &error); 107 | 108 | if (error == NULL) { 109 | wordexp_t p; 110 | if (wordexp(save_dir, &p, 0) == 0) { 111 | save_dir_expanded = g_strdup(p.we_wordv[0]); 112 | wordfree(&p); 113 | if (!save_dir_expanded || !folder_exists(save_dir_expanded)) { 114 | g_info("save_dir: attempting to create non-existent directory '%s'", 115 | save_dir_expanded); 116 | if (g_mkdir_with_parents(save_dir_expanded, 0755)) { 117 | g_warning("save_dir: failed to create '%s'", save_dir_expanded); 118 | } 119 | } 120 | 121 | g_free(save_dir); 122 | g_free(config->save_dir); 123 | config->save_dir = save_dir_expanded; 124 | } 125 | } else { 126 | g_info("save_dir is missing in %s (%s)", file, error->message); 127 | g_error_free(error); 128 | error = NULL; 129 | } 130 | 131 | save_filename_format = 132 | g_key_file_get_string(gkf, group, "save_filename_format", &error); 133 | 134 | if (error == NULL) { 135 | config->save_filename_format = save_filename_format; 136 | } else { 137 | g_info("save_filename_format is missing in %s (%s)", file, error->message); 138 | g_error_free(error); 139 | error = NULL; 140 | } 141 | 142 | line_size = g_key_file_get_uint64(gkf, group, "line_size", &error); 143 | 144 | if (error == NULL) { 145 | if (line_size >= SWAPPY_LINE_SIZE_MIN && 146 | line_size <= SWAPPY_LINE_SIZE_MAX) { 147 | config->line_size = line_size; 148 | } else { 149 | g_warning("line_size is not a valid value: %" PRIu64 150 | " - see man page for details", 151 | line_size); 152 | } 153 | } else { 154 | g_info("line_size is missing in %s (%s)", file, error->message); 155 | g_error_free(error); 156 | error = NULL; 157 | } 158 | 159 | text_size = g_key_file_get_uint64(gkf, group, "text_size", &error); 160 | 161 | if (error == NULL) { 162 | if (text_size >= SWAPPY_TEXT_SIZE_MIN && 163 | text_size <= SWAPPY_TEXT_SIZE_MAX) { 164 | config->text_size = text_size; 165 | } else { 166 | g_warning("text_size is not a valid value: %" PRIu64 167 | " - see man page for details", 168 | text_size); 169 | } 170 | } else { 171 | g_info("text_size is missing in %s (%s)", file, error->message); 172 | g_error_free(error); 173 | error = NULL; 174 | } 175 | 176 | text_font = g_key_file_get_string(gkf, group, "text_font", &error); 177 | 178 | if (error == NULL) { 179 | g_free(config->text_font); 180 | config->text_font = text_font; 181 | } else { 182 | g_info("text_font is missing in %s (%s)", file, error->message); 183 | g_error_free(error); 184 | error = NULL; 185 | } 186 | 187 | transparency = g_key_file_get_uint64(gkf, group, "transparency", &error); 188 | 189 | if (error == NULL) { 190 | if (transparency >= SWAPPY_TRANSPARENCY_MIN && 191 | transparency <= SWAPPY_TRANSPARENCY_MAX) { 192 | config->transparency = transparency; 193 | } else { 194 | g_warning("transparency is not a valid value: %" PRIu64 195 | " - see man page for details", 196 | transparency); 197 | } 198 | } else { 199 | g_info("transparency is missing in %s (%s)", file, error->message); 200 | g_error_free(error); 201 | error = NULL; 202 | } 203 | 204 | show_panel = g_key_file_get_boolean(gkf, group, "show_panel", &error); 205 | 206 | if (error == NULL) { 207 | config->show_panel = show_panel; 208 | } else { 209 | g_info("show_panel is missing in %s (%s)", file, error->message); 210 | g_error_free(error); 211 | error = NULL; 212 | } 213 | 214 | early_exit = g_key_file_get_boolean(gkf, group, "early_exit", &error); 215 | 216 | if (error == NULL) { 217 | config->early_exit = early_exit; 218 | } else { 219 | g_info("early_exit is missing in %s (%s)", file, error->message); 220 | g_error_free(error); 221 | error = NULL; 222 | } 223 | 224 | paint_mode = g_key_file_get_string(gkf, group, "paint_mode", &error); 225 | 226 | if (error == NULL) { 227 | if (g_ascii_strcasecmp(paint_mode, "brush") == 0) { 228 | config->paint_mode = SWAPPY_PAINT_MODE_BRUSH; 229 | } else if (g_ascii_strcasecmp(paint_mode, "text") == 0) { 230 | config->paint_mode = SWAPPY_PAINT_MODE_TEXT; 231 | } else if (g_ascii_strcasecmp(paint_mode, "rectangle") == 0) { 232 | config->paint_mode = SWAPPY_PAINT_MODE_RECTANGLE; 233 | } else if (g_ascii_strcasecmp(paint_mode, "ellipse") == 0) { 234 | config->paint_mode = SWAPPY_PAINT_MODE_ELLIPSE; 235 | } else if (g_ascii_strcasecmp(paint_mode, "arrow") == 0) { 236 | config->paint_mode = SWAPPY_PAINT_MODE_ARROW; 237 | } else if (g_ascii_strcasecmp(paint_mode, "blur") == 0) { 238 | config->paint_mode = SWAPPY_PAINT_MODE_BLUR; 239 | } else { 240 | g_warning( 241 | "paint_mode is not a valid value: %s - see man page for details", 242 | paint_mode); 243 | } 244 | } else { 245 | g_info("paint_mode is missing in %s (%s)", file, error->message); 246 | g_error_free(error); 247 | error = NULL; 248 | } 249 | 250 | fill_shape = g_key_file_get_boolean(gkf, group, "fill_shape", &error); 251 | 252 | if (error == NULL) { 253 | config->fill_shape = fill_shape; 254 | } else { 255 | g_info("fill_shape is missing in %s (%s)", file, error->message); 256 | g_error_free(error); 257 | error = NULL; 258 | } 259 | 260 | auto_save = g_key_file_get_boolean(gkf, group, "auto_save", &error); 261 | 262 | if (error == NULL) { 263 | config->auto_save = auto_save; 264 | } else { 265 | g_info("auto_save is missing in %s (%s)", file, error->message); 266 | g_error_free(error); 267 | error = NULL; 268 | } 269 | 270 | custom_color = g_key_file_get_string(gkf, group, "custom_color", &error); 271 | 272 | if (error == NULL) { 273 | config->custom_color = custom_color; 274 | } else { 275 | g_info("custom_color is missing in %s (%s)", file, error->message); 276 | g_error_free(error); 277 | error = NULL; 278 | } 279 | 280 | transparent = g_key_file_get_boolean(gkf, group, "transparent", &error); 281 | 282 | if (error == NULL) { 283 | config->transparent = transparent; 284 | } else { 285 | g_info("transparent is missing in %s (%s)", file, error->message); 286 | g_error_free(error); 287 | error = NULL; 288 | } 289 | 290 | g_key_file_free(gkf); 291 | } 292 | 293 | static void load_default_config(struct swappy_config *config) { 294 | if (config == NULL) { 295 | return; 296 | } 297 | 298 | config->save_dir = get_default_save_dir(); 299 | config->save_filename_format = g_strdup(CONFIG_SAVE_FILENAME_FORMAT_DEFAULT); 300 | config->line_size = CONFIG_LINE_SIZE_DEFAULT; 301 | config->text_font = g_strdup(CONFIG_TEXT_FONT_DEFAULT); 302 | config->text_size = CONFIG_TEXT_SIZE_DEFAULT; 303 | config->show_panel = CONFIG_SHOW_PANEL_DEFAULT; 304 | config->paint_mode = CONFIG_PAINT_MODE_DEFAULT; 305 | config->early_exit = CONFIG_EARLY_EXIT_DEFAULT; 306 | config->fill_shape = CONFIG_FILL_SHAPE_DEFAULT; 307 | config->auto_save = CONFIG_AUTO_SAVE_DEFAULT; 308 | config->custom_color = g_strdup(CONFIG_CUSTOM_COLOR_DEFAULT); 309 | config->transparent = CONFIG_TRANSPARENT_DEFAULT; 310 | config->transparency = CONFIG_TRANSPARENCY_DEFAULT; 311 | } 312 | 313 | void config_load(struct swappy_state *state) { 314 | struct swappy_config *config = g_new(struct swappy_config, 1); 315 | 316 | load_default_config(config); 317 | 318 | char *file = get_config_file(); 319 | if (file) { 320 | load_config_from_file(config, file); 321 | } else { 322 | g_info("could not find swappy config file, using defaults"); 323 | } 324 | 325 | config->config_file = file; 326 | state->config = config; 327 | 328 | print_config(state->config); 329 | } 330 | 331 | void config_free(struct swappy_state *state) { 332 | if (state->config) { 333 | g_free(state->config->config_file); 334 | g_free(state->config->save_dir); 335 | g_free(state->config->save_filename_format); 336 | g_free(state->config->text_font); 337 | g_free(state->config->custom_color); 338 | g_free(state->config); 339 | state->config = NULL; 340 | } 341 | } 342 | -------------------------------------------------------------------------------- /src/file.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200112L 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #define BLOCK_SIZE 1024 13 | 14 | bool folder_exists(const char *path) { 15 | return g_file_test(path, G_FILE_TEST_IS_DIR); 16 | } 17 | 18 | bool file_exists(const char *path) { 19 | return g_file_test(path, G_FILE_TEST_EXISTS); 20 | } 21 | 22 | char *file_dump_stdin_into_a_temp_file() { 23 | char buf[BLOCK_SIZE]; 24 | GError *error = NULL; 25 | 26 | if (isatty(STDIN_FILENO)) { 27 | g_warning("stdin is a tty"); 28 | return NULL; 29 | } 30 | 31 | // Reopen stdin as binary mode 32 | FILE *input_file = g_freopen(NULL, "rb", stdin); 33 | 34 | if (!input_file) { 35 | g_warning("unable to reopen stdin in binary mode: %s", g_strerror(errno)); 36 | return NULL; 37 | } 38 | 39 | const gchar *tempdir = g_get_tmp_dir(); 40 | gchar filename[] = "swappy-stdin-XXXXXX.png"; 41 | gchar *ret = g_build_filename(tempdir, filename, NULL); 42 | gint fd = g_mkstemp(ret); 43 | 44 | if (fd == -1) { 45 | g_warning("unable to dump stdin into temporary file"); 46 | return NULL; 47 | } 48 | 49 | g_info("writing stdin content into filepath: %s", ret); 50 | 51 | size_t count = 1; 52 | while (count > 0) { 53 | count = fread(buf, 1, sizeof(buf), stdin); 54 | if (write(fd, &buf, count) == -1) { 55 | g_warning("error while writing stdin to temporary file: %s - %s", ret, 56 | g_strerror(errno)); 57 | } 58 | } 59 | 60 | g_close(fd, &error); 61 | 62 | if (error) { 63 | g_warning("unable to close temporary file: %s", error->message); 64 | g_error_free(error); 65 | return NULL; 66 | } 67 | 68 | return ret; 69 | } 70 | -------------------------------------------------------------------------------- /src/main.c: -------------------------------------------------------------------------------- 1 | #define _POSIX_C_SOURCE 200809L 2 | 3 | #include "application.h" 4 | #include "config.h" 5 | 6 | int main(int argc, char *argv[]) { 7 | struct swappy_state state = {0}; 8 | int status; 9 | 10 | state.argc = argc; 11 | state.argv = argv; 12 | state.mode = SWAPPY_PAINT_MODE_BRUSH; 13 | 14 | if (!application_init(&state)) { 15 | g_critical("failed to initialize gtk application"); 16 | exit(1); 17 | } 18 | 19 | status = application_run(&state); 20 | 21 | if (status == 0) { 22 | gtk_main(); 23 | } 24 | 25 | application_finish(&state); 26 | 27 | return status; 28 | } 29 | -------------------------------------------------------------------------------- /src/paint.c: -------------------------------------------------------------------------------- 1 | #include "paint.h" 2 | 3 | #include 4 | #include 5 | 6 | #include "gtk/gtk.h" 7 | #include "util.h" 8 | 9 | static void cursor_move_backward(struct swappy_paint_text *text) { 10 | if (text->cursor > 0) { 11 | text->cursor--; 12 | } 13 | } 14 | 15 | static void cursor_move_forward(struct swappy_paint_text *text) { 16 | if (text->cursor < g_utf8_strlen(text->text, -1)) { 17 | text->cursor++; 18 | } 19 | } 20 | 21 | void paint_free(gpointer data) { 22 | struct swappy_paint *paint = (struct swappy_paint *)data; 23 | 24 | if (paint == NULL) { 25 | return; 26 | } 27 | 28 | switch (paint->type) { 29 | case SWAPPY_PAINT_MODE_BLUR: 30 | if (paint->content.blur.surface) { 31 | cairo_surface_destroy(paint->content.blur.surface); 32 | } 33 | break; 34 | case SWAPPY_PAINT_MODE_BRUSH: 35 | g_list_free_full(paint->content.brush.points, g_free); 36 | break; 37 | case SWAPPY_PAINT_MODE_TEXT: 38 | g_free(paint->content.text.text); 39 | g_free(paint->content.text.font); 40 | break; 41 | default: 42 | break; 43 | } 44 | g_free(paint); 45 | } 46 | 47 | void paint_free_list(GList **list) { 48 | if (*list) { 49 | g_list_free_full(*list, paint_free); 50 | *list = NULL; 51 | } 52 | } 53 | 54 | void paint_free_all(struct swappy_state *state) { 55 | paint_free_list(&state->paints); 56 | paint_free_list(&state->redo_paints); 57 | paint_free(state->temp_paint); 58 | state->temp_paint = NULL; 59 | } 60 | 61 | void paint_add_temporary(struct swappy_state *state, double x, double y, 62 | enum swappy_paint_type type) { 63 | struct swappy_paint *paint = g_new(struct swappy_paint, 1); 64 | struct swappy_point *point; 65 | 66 | double r = state->settings.r; 67 | double g = state->settings.g; 68 | double b = state->settings.b; 69 | double a = state->settings.a; 70 | double w = state->settings.w; 71 | double t = state->settings.t; 72 | 73 | if (state->config->transparent) a *= (1 - state->settings.tr / 100.0); 74 | 75 | paint->type = type; 76 | paint->is_committed = false; 77 | 78 | g_debug("adding temporary paint at: %.2lfx%.2lf", x, y); 79 | 80 | if (state->temp_paint) { 81 | if (type == SWAPPY_PAINT_MODE_TEXT) { 82 | paint_commit_temporary(state); 83 | } else { 84 | paint_free(state->temp_paint); 85 | state->temp_paint = NULL; 86 | } 87 | } 88 | 89 | switch (type) { 90 | case SWAPPY_PAINT_MODE_BLUR: 91 | paint->can_draw = false; 92 | 93 | paint->content.blur.from.x = x; 94 | paint->content.blur.from.y = y; 95 | paint->content.blur.surface = NULL; 96 | break; 97 | case SWAPPY_PAINT_MODE_BRUSH: 98 | paint->can_draw = true; 99 | 100 | paint->content.brush.r = r; 101 | paint->content.brush.g = g; 102 | paint->content.brush.b = b; 103 | paint->content.brush.a = a; 104 | paint->content.brush.w = w; 105 | 106 | point = g_new(struct swappy_point, 1); 107 | point->x = x; 108 | point->y = y; 109 | 110 | paint->content.brush.points = g_list_prepend(NULL, point); 111 | break; 112 | case SWAPPY_PAINT_MODE_RECTANGLE: 113 | case SWAPPY_PAINT_MODE_ELLIPSE: 114 | case SWAPPY_PAINT_MODE_ARROW: 115 | paint->can_draw = false; // need `to` vector 116 | 117 | paint->content.shape.from.x = x; 118 | paint->content.shape.from.y = y; 119 | paint->content.shape.r = r; 120 | paint->content.shape.g = g; 121 | paint->content.shape.b = b; 122 | paint->content.shape.a = a; 123 | paint->content.shape.w = w; 124 | paint->content.shape.type = type; 125 | if (state->config->fill_shape) 126 | paint->content.shape.operation = SWAPPY_PAINT_SHAPE_OPERATION_FILL; 127 | else 128 | paint->content.shape.operation = SWAPPY_PAINT_SHAPE_OPERATION_STROKE; 129 | break; 130 | case SWAPPY_PAINT_MODE_TEXT: 131 | paint->can_draw = false; 132 | 133 | paint->content.text.from.x = x; 134 | paint->content.text.from.y = y; 135 | paint->content.text.r = r; 136 | paint->content.text.g = g; 137 | paint->content.text.b = b; 138 | paint->content.text.a = a; 139 | paint->content.text.s = t; 140 | paint->content.text.font = g_strdup(state->config->text_font); 141 | paint->content.text.cursor = 0; 142 | paint->content.text.mode = SWAPPY_TEXT_MODE_EDIT; 143 | paint->content.text.text = g_new(gchar, 1); 144 | paint->content.text.text[0] = '\0'; 145 | break; 146 | 147 | default: 148 | g_info("unable to add temporary paint: %d", type); 149 | break; 150 | } 151 | 152 | state->temp_paint = paint; 153 | } 154 | 155 | void paint_update_temporary_shape(struct swappy_state *state, double x, 156 | double y, gboolean is_control_pressed) { 157 | struct swappy_paint *paint = state->temp_paint; 158 | struct swappy_point *point; 159 | GList *points; 160 | 161 | if (!paint) { 162 | return; 163 | } 164 | 165 | switch (paint->type) { 166 | case SWAPPY_PAINT_MODE_BLUR: 167 | paint->can_draw = true; 168 | paint->content.blur.to.x = x; 169 | paint->content.blur.to.y = y; 170 | break; 171 | case SWAPPY_PAINT_MODE_BRUSH: 172 | points = paint->content.brush.points; 173 | point = g_new(struct swappy_point, 1); 174 | point->x = x; 175 | point->y = y; 176 | 177 | paint->content.brush.points = g_list_prepend(points, point); 178 | break; 179 | case SWAPPY_PAINT_MODE_RECTANGLE: 180 | case SWAPPY_PAINT_MODE_ELLIPSE: 181 | paint->can_draw = true; // all set 182 | 183 | paint->content.shape.should_center_at_from = is_control_pressed; 184 | paint->content.shape.to.x = x; 185 | paint->content.shape.to.y = y; 186 | break; 187 | case SWAPPY_PAINT_MODE_ARROW: 188 | paint->can_draw = true; // all set 189 | 190 | paint->content.shape.to.x = x; 191 | paint->content.shape.to.y = y; 192 | break; 193 | default: 194 | g_info("unable to update temporary paint when type is: %d", paint->type); 195 | break; 196 | } 197 | } 198 | 199 | void paint_update_temporary_str(struct swappy_state *state, char *str) { 200 | struct swappy_paint *paint = state->temp_paint; 201 | struct swappy_paint_text *text; 202 | char *new_text; 203 | if (!paint || paint->type != SWAPPY_PAINT_MODE_TEXT) { 204 | g_warning("trying to update text but not in text mode"); 205 | return; 206 | } 207 | 208 | text = &paint->content.text; 209 | new_text = string_insert_chars_at(text->text, str, text->cursor); 210 | g_free(text->text); 211 | text->text = new_text; 212 | text->cursor += g_utf8_strlen(str, -1); 213 | } 214 | 215 | void paint_update_temporary_text(struct swappy_state *state, 216 | GdkEventKey *event) { 217 | struct swappy_paint *paint = state->temp_paint; 218 | struct swappy_paint_text *text; 219 | char *new_text; 220 | char buffer[32]; 221 | guint32 unicode; 222 | 223 | if (!paint || paint->type != SWAPPY_PAINT_MODE_TEXT) { 224 | g_warning("trying to update text but not in text mode"); 225 | return; 226 | } 227 | 228 | text = &paint->content.text; 229 | 230 | switch (event->keyval) { 231 | case GDK_KEY_Escape: 232 | paint_commit_temporary(state); 233 | break; 234 | case GDK_KEY_BackSpace: 235 | if (g_utf8_strlen(text->text, -1) > 0) { 236 | new_text = string_remove_at(text->text, text->cursor - 1); 237 | g_free(text->text); 238 | text->text = new_text; 239 | cursor_move_backward(text); 240 | } 241 | break; 242 | case GDK_KEY_Delete: 243 | if (g_utf8_strlen(text->text, -1) > 0) { 244 | new_text = string_remove_at(text->text, text->cursor); 245 | g_free(text->text); 246 | text->text = new_text; 247 | } 248 | break; 249 | case GDK_KEY_Left: 250 | cursor_move_backward(text); 251 | break; 252 | case GDK_KEY_Right: 253 | cursor_move_forward(text); 254 | break; 255 | case GDK_KEY_V: 256 | cursor_move_forward(text); 257 | break; 258 | default: 259 | unicode = gdk_keyval_to_unicode(event->keyval); 260 | if (unicode != 0) { 261 | int ll = g_unichar_to_utf8(unicode, buffer); 262 | buffer[ll] = '\0'; 263 | char *new_text = 264 | string_insert_chars_at(text->text, buffer, text->cursor); 265 | g_free(text->text); 266 | text->text = new_text; 267 | text->cursor++; 268 | } 269 | break; 270 | } 271 | } 272 | 273 | void paint_update_temporary_text_clip(struct swappy_state *state, gdouble x, 274 | gdouble y) { 275 | struct swappy_paint *paint = state->temp_paint; 276 | 277 | if (!paint) { 278 | return; 279 | } 280 | 281 | g_assert(paint->type == SWAPPY_PAINT_MODE_TEXT); 282 | 283 | paint->can_draw = true; 284 | paint->content.text.to.x = x; 285 | paint->content.text.to.y = y; 286 | gtk_im_context_focus_in(state->ui->im_context); 287 | } 288 | 289 | void paint_commit_temporary(struct swappy_state *state) { 290 | struct swappy_paint *paint = state->temp_paint; 291 | 292 | if (!paint) { 293 | return; 294 | } 295 | 296 | switch (paint->type) { 297 | case SWAPPY_PAINT_MODE_TEXT: 298 | if (g_utf8_strlen(paint->content.text.text, -1) == 0) { 299 | paint->can_draw = false; 300 | } 301 | paint->content.text.mode = SWAPPY_TEXT_MODE_DONE; 302 | break; 303 | default: 304 | break; 305 | } 306 | 307 | if (!paint->can_draw) { 308 | paint_free(paint); 309 | } else { 310 | paint->is_committed = true; 311 | state->paints = g_list_prepend(state->paints, paint); 312 | } 313 | 314 | gtk_im_context_focus_out(state->ui->im_context); 315 | // Set the temporary paint to NULL but keep the content in memory 316 | // because it's now part of the GList. 317 | state->temp_paint = NULL; 318 | } 319 | -------------------------------------------------------------------------------- /src/pixbuf.c: -------------------------------------------------------------------------------- 1 | #include "pixbuf.h" 2 | 3 | #include 4 | #include 5 | 6 | GdkPixbuf *pixbuf_get_from_state(struct swappy_state *state) { 7 | guint width = cairo_image_surface_get_width(state->rendering_surface); 8 | guint height = cairo_image_surface_get_height(state->rendering_surface); 9 | GdkPixbuf *pixbuf = gdk_pixbuf_get_from_surface(state->rendering_surface, 0, 10 | 0, width, height); 11 | 12 | return pixbuf; 13 | } 14 | 15 | static void write_file(GdkPixbuf *pixbuf, char *path) { 16 | GError *error = NULL; 17 | gdk_pixbuf_savev(pixbuf, path, "png", NULL, NULL, &error); 18 | 19 | if (error != NULL) { 20 | g_critical("unable to save drawing area to pixbuf: %s", error->message); 21 | g_error_free(error); 22 | } 23 | } 24 | 25 | void pixbuf_save_state_to_folder(GdkPixbuf *pixbuf, char *folder, 26 | char *filename_format) { 27 | time_t current_time = time(NULL); 28 | char *c_time_string; 29 | char filename[255]; 30 | char path[MAX_PATH]; 31 | size_t bytes_formated; 32 | 33 | c_time_string = ctime(¤t_time); 34 | c_time_string[strlen(c_time_string) - 1] = '\0'; 35 | bytes_formated = strftime(filename, sizeof(filename), filename_format, 36 | localtime(¤t_time)); 37 | if (!bytes_formated) { 38 | g_warning( 39 | "filename_format: %s overflows filename limit - file cannot be saved", 40 | filename_format); 41 | return; 42 | } 43 | 44 | g_snprintf(path, MAX_PATH, "%s/%s", folder, filename); 45 | g_info("saving surface to path: %s", path); 46 | write_file(pixbuf, path); 47 | } 48 | 49 | void pixbuf_save_to_stdout(GdkPixbuf *pixbuf) { 50 | GOutputStream *out; 51 | GError *error = NULL; 52 | 53 | out = g_unix_output_stream_new(STDOUT_FILENO, TRUE); 54 | 55 | gdk_pixbuf_save_to_stream(pixbuf, out, "png", NULL, &error, NULL); 56 | 57 | if (error != NULL) { 58 | g_warning("unable to save surface to stdout: %s", error->message); 59 | g_error_free(error); 60 | return; 61 | } 62 | 63 | g_object_unref(out); 64 | } 65 | 66 | GdkPixbuf *pixbuf_init_from_file(struct swappy_state *state) { 67 | GError *error = NULL; 68 | char *file = 69 | state->temp_file_str != NULL ? state->temp_file_str : state->file_str; 70 | GdkPixbuf *image = gdk_pixbuf_new_from_file(file, &error); 71 | 72 | if (error != NULL) { 73 | g_printerr("unable to load file: %s - reason: %s\n", file, error->message); 74 | g_error_free(error); 75 | return NULL; 76 | } 77 | 78 | state->original_image = image; 79 | return image; 80 | } 81 | 82 | void pixbuf_save_to_file(GdkPixbuf *pixbuf, char *file) { 83 | if (g_strcmp0(file, "-") == 0) { 84 | pixbuf_save_to_stdout(pixbuf); 85 | } else { 86 | write_file(pixbuf, file); 87 | } 88 | } 89 | 90 | void pixbuf_scale_surface_from_widget(struct swappy_state *state, 91 | GtkWidget *widget) { 92 | GtkAllocation *alloc = g_new(GtkAllocation, 1); 93 | GdkPixbuf *image = state->original_image; 94 | gtk_widget_get_allocation(widget, alloc); 95 | 96 | cairo_format_t format = CAIRO_FORMAT_ARGB32; 97 | gint image_width = gdk_pixbuf_get_width(image); 98 | gint image_height = gdk_pixbuf_get_height(image); 99 | 100 | cairo_surface_t *original_image_surface = 101 | cairo_image_surface_create(format, image_width, image_height); 102 | 103 | if (!original_image_surface) { 104 | g_error("unable to create cairo original surface from pixbuf"); 105 | goto finish; 106 | } else { 107 | cairo_t *cr; 108 | cr = cairo_create(original_image_surface); 109 | gdk_cairo_set_source_pixbuf(cr, image, 0, 0); 110 | cairo_paint(cr); 111 | cairo_destroy(cr); 112 | } 113 | 114 | cairo_surface_t *rendering_surface = 115 | cairo_image_surface_create(format, image_width, image_height); 116 | 117 | if (!rendering_surface) { 118 | g_error("unable to create rendering surface"); 119 | goto finish; 120 | } 121 | 122 | g_info("size of area to render: %ux%u", alloc->width, alloc->height); 123 | 124 | finish: 125 | if (state->original_image_surface) { 126 | cairo_surface_destroy(state->original_image_surface); 127 | state->original_image_surface = NULL; 128 | } 129 | state->original_image_surface = original_image_surface; 130 | 131 | if (state->rendering_surface) { 132 | cairo_surface_destroy(state->rendering_surface); 133 | state->rendering_surface = NULL; 134 | } 135 | state->rendering_surface = rendering_surface; 136 | 137 | g_free(alloc); 138 | } 139 | 140 | void pixbuf_free(struct swappy_state *state) { 141 | if (G_IS_OBJECT(state->original_image)) { 142 | g_object_unref(state->original_image); 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /src/po/LINGUAS: -------------------------------------------------------------------------------- 1 | # Set of available languages. 2 | de 3 | fr 4 | en 5 | pt_BR 6 | tr 7 | zh_CN 8 | -------------------------------------------------------------------------------- /src/po/POTFILES: -------------------------------------------------------------------------------- 1 | res/swappy.glade 2 | -------------------------------------------------------------------------------- /src/po/de.po: -------------------------------------------------------------------------------- 1 | # German translations for swappy package. 2 | # Copyright (C) 2020 THE swappy'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the swappy package. 4 | # Brodi , 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: swappy\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n" 11 | "PO-Revision-Date: 2020-11-19 18:03+0300\n" 12 | "Last-Translator: Brodi \n" 13 | "Language-Team: none\n" 14 | "Language: de\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: res/swappy.glade:461 21 | msgid "Line Width" 22 | msgstr "Linienstärke" 23 | 24 | #: res/swappy.glade:531 25 | msgid "Text Size" 26 | msgstr "Textgröße" 27 | 28 | #: res/swappy.glade:601 29 | msgid "Transparency" 30 | msgstr "" 31 | 32 | #: res/swappy.glade:667 33 | msgid "Fill shape" 34 | msgstr "" 35 | 36 | #: res/swappy.glade:672 37 | msgid "Toggle shape filling" 38 | msgstr "" 39 | 40 | #: res/swappy.glade:684 41 | msgid "Transparent" 42 | msgstr "" 43 | 44 | #: res/swappy.glade:689 45 | msgid "Toggle transparency" 46 | msgstr "" 47 | 48 | #: res/swappy.glade:763 49 | msgid "Toggle Paint Panel" 50 | msgstr "Farbtafel umschalten" 51 | 52 | #: res/swappy.glade:789 53 | msgid "Undo Last Paint" 54 | msgstr "Letzte Bemalung rückgängig machen" 55 | 56 | #: res/swappy.glade:808 57 | msgid "Redo Previous Paint" 58 | msgstr "Vorherige Bemalung wiederherstellen" 59 | 60 | #: res/swappy.glade:827 61 | msgid "Clear Paints" 62 | msgstr "Bemalung löschen" 63 | 64 | #: res/swappy.glade:855 65 | msgid "Copy Surface" 66 | msgstr "Fläche kopieren" 67 | 68 | #: res/swappy.glade:871 69 | msgid "Save Surface" 70 | msgstr "Fläche speichern" 71 | -------------------------------------------------------------------------------- /src/po/en.po: -------------------------------------------------------------------------------- 1 | # English translations for swappy package. 2 | # Copyright (C) 2020 THE swappy'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the swappy package. 4 | # Automatically generated, 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: swappy\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n" 11 | "PO-Revision-Date: 2020-06-21 21:57-0400\n" 12 | "Last-Translator: Automatically generated\n" 13 | "Language-Team: none\n" 14 | "Language: en\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=ASCII\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: res/swappy.glade:461 21 | msgid "Line Width" 22 | msgstr "Line Width" 23 | 24 | #: res/swappy.glade:531 25 | msgid "Text Size" 26 | msgstr "Text Size" 27 | 28 | #: res/swappy.glade:601 29 | msgid "Transparency" 30 | msgstr "" 31 | 32 | #: res/swappy.glade:667 33 | msgid "Fill shape" 34 | msgstr "Fill shape" 35 | 36 | #: res/swappy.glade:672 37 | msgid "Toggle shape filling" 38 | msgstr "Toggle shape filling" 39 | 40 | #: res/swappy.glade:684 41 | msgid "Transparent" 42 | msgstr "" 43 | 44 | #: res/swappy.glade:689 45 | msgid "Toggle transparency" 46 | msgstr "" 47 | 48 | #: res/swappy.glade:763 49 | msgid "Toggle Paint Panel" 50 | msgstr "Toggle Paint Panel" 51 | 52 | #: res/swappy.glade:789 53 | msgid "Undo Last Paint" 54 | msgstr "Undo Last Paint" 55 | 56 | #: res/swappy.glade:808 57 | msgid "Redo Previous Paint" 58 | msgstr "Redo Previous Paint" 59 | 60 | #: res/swappy.glade:827 61 | msgid "Clear Paints" 62 | msgstr "Clear Paints" 63 | 64 | #: res/swappy.glade:855 65 | msgid "Copy Surface" 66 | msgstr "Copy Surface" 67 | 68 | #: res/swappy.glade:871 69 | msgid "Save Surface" 70 | msgstr "Save Surface" 71 | -------------------------------------------------------------------------------- /src/po/fr.po: -------------------------------------------------------------------------------- 1 | # French translations for swappy package. 2 | # Copyright (C) 2021 THE swappy'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the swappy package. 4 | # Jeremy Attali , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: swappy\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n" 11 | "PO-Revision-Date: 2021-02-20 21:00-0500\n" 12 | "Last-Translator: Jeremy Attali \n" 13 | "Language-Team: none\n" 14 | "Language: fr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 19 | 20 | #: res/swappy.glade:461 21 | msgid "Line Width" 22 | msgstr "Epaisseur de ligne" 23 | 24 | #: res/swappy.glade:531 25 | msgid "Text Size" 26 | msgstr "Taille du texte" 27 | 28 | #: res/swappy.glade:601 29 | msgid "Transparency" 30 | msgstr "" 31 | 32 | #: res/swappy.glade:667 33 | msgid "Fill shape" 34 | msgstr "Remplir la forme" 35 | 36 | #: res/swappy.glade:672 37 | msgid "Toggle shape filling" 38 | msgstr "Activer/Désactiver le remplissage de forme" 39 | 40 | #: res/swappy.glade:684 41 | msgid "Transparent" 42 | msgstr "" 43 | 44 | #: res/swappy.glade:689 45 | msgid "Toggle transparency" 46 | msgstr "" 47 | 48 | #: res/swappy.glade:763 49 | msgid "Toggle Paint Panel" 50 | msgstr "Afficher/Cacher le panneau de peinture" 51 | 52 | #: res/swappy.glade:789 53 | msgid "Undo Last Paint" 54 | msgstr "Annuler la dernière peinture" 55 | 56 | #: res/swappy.glade:808 57 | msgid "Redo Previous Paint" 58 | msgstr "Rétablir la dernière peinture" 59 | 60 | #: res/swappy.glade:827 61 | msgid "Clear Paints" 62 | msgstr "Supprimer les peintures" 63 | 64 | #: res/swappy.glade:855 65 | msgid "Copy Surface" 66 | msgstr "Copier la surface" 67 | 68 | #: res/swappy.glade:871 69 | msgid "Save Surface" 70 | msgstr "Sauvegarder la surface" 71 | -------------------------------------------------------------------------------- /src/po/meson.build: -------------------------------------------------------------------------------- 1 | i18n = import('i18n') 2 | 3 | # define GETTEXT_PACKAGE 4 | add_project_arguments('-DGETTEXT_PACKAGE="swappy"', language:'c') 5 | 6 | i18n.gettext(meson.project_name(), 7 | args: '--directory=' + meson.source_root() 8 | ) 9 | 10 | # Translate and install our .desktop file 11 | i18n.merge_file( 12 | input: meson.project_name() + '.desktop.in', 13 | output: meson.project_name() + '.desktop', 14 | po_dir: meson.current_source_dir(), 15 | type: 'desktop', 16 | install: true, 17 | install_dir: join_paths(get_option('datadir'), 'applications') 18 | ) 19 | -------------------------------------------------------------------------------- /src/po/pt_BR.po: -------------------------------------------------------------------------------- 1 | # Brazilian Portuguese translation for swappy 2 | # Copyright (C) 2020 Jeremy Attali 3 | # This file is distributed under the MIT License 4 | # Gustavo Costa , 2021. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: swappy\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n" 11 | "PO-Revision-Date: 2021-02-14 20:38-0300\n" 12 | "Last-Translator: Gustavo Costa \n" 13 | "Language-Team: \n" 14 | "Language: pt_BR\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "X-Generator: Poedit 2.4.2\n" 19 | "Plural-Forms: nplurals=2; plural=(n > 1);\n" 20 | 21 | #: res/swappy.glade:461 22 | msgid "Line Width" 23 | msgstr "Espessura da linha" 24 | 25 | #: res/swappy.glade:531 26 | msgid "Text Size" 27 | msgstr "Tamanho do texto" 28 | 29 | #: res/swappy.glade:601 30 | msgid "Transparency" 31 | msgstr "" 32 | 33 | #: res/swappy.glade:667 34 | msgid "Fill shape" 35 | msgstr "" 36 | 37 | #: res/swappy.glade:672 38 | msgid "Toggle shape filling" 39 | msgstr "" 40 | 41 | #: res/swappy.glade:684 42 | msgid "Transparent" 43 | msgstr "" 44 | 45 | #: res/swappy.glade:689 46 | msgid "Toggle transparency" 47 | msgstr "" 48 | 49 | #: res/swappy.glade:763 50 | msgid "Toggle Paint Panel" 51 | msgstr "Alternar painel de pintura" 52 | 53 | #: res/swappy.glade:789 54 | msgid "Undo Last Paint" 55 | msgstr "Desfazer última pintura" 56 | 57 | #: res/swappy.glade:808 58 | msgid "Redo Previous Paint" 59 | msgstr "Refazer pintura anterior" 60 | 61 | #: res/swappy.glade:827 62 | msgid "Clear Paints" 63 | msgstr "Limpar pinturas" 64 | 65 | #: res/swappy.glade:855 66 | msgid "Copy Surface" 67 | msgstr "Copiar superfície" 68 | 69 | #: res/swappy.glade:871 70 | msgid "Save Surface" 71 | msgstr "Salvar superfície" 72 | -------------------------------------------------------------------------------- /src/po/swappy.desktop.in: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=Swappy 3 | GenericName=Annotation Tool 4 | GenericName[de]=Anmerkungswerkzeug 5 | GenericName[fr]=Outil d'annotation 6 | GenericName[pt_BR]=Ferramenta de Anotação 7 | GenericName[tr]=Açıklama Aracı 8 | Comment=A Wayland native snapshot editing tool 9 | Comment[de]=Ein natives Wayland Bildschirmfoto-Bearbeitungswerkzeug 10 | Comment[fr]=Un outil d'édition de capture d'écran avec support natif pour Wayland 11 | Comment[pt_BR]=Uma ferramenta de edição de snapshot nativa do Wayland 12 | Comment[tr]=Wayland için anlık görüntü düzenleme aracı 13 | TryExec=swappy 14 | Exec=swappy -f %f 15 | Terminal=false 16 | NoDisplay=true 17 | Type=Application 18 | Keywords=wayland;snapshot;annotation;editing; 19 | Icon=swappy 20 | Categories=Utility;Graphics; 21 | StartupNotify=true 22 | MimeType=image/png;image/jpeg; 23 | -------------------------------------------------------------------------------- /src/po/swappy.pot: -------------------------------------------------------------------------------- 1 | # SOME DESCRIPTIVE TITLE. 2 | # Copyright (C) YEAR THE PACKAGE'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the swappy package. 4 | # FIRST AUTHOR , YEAR. 5 | # 6 | #, fuzzy 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: swappy\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2024-11-06 23:00+0100\n" 12 | "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" 13 | "Last-Translator: FULL NAME \n" 14 | "Language-Team: LANGUAGE \n" 15 | "Language: \n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=CHARSET\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | 20 | #: res/swappy.glade:461 21 | msgid "Line Width" 22 | msgstr "" 23 | 24 | #: res/swappy.glade:531 25 | msgid "Text Size" 26 | msgstr "" 27 | 28 | #: res/swappy.glade:601 29 | msgid "Transparency" 30 | msgstr "" 31 | 32 | #: res/swappy.glade:667 33 | msgid "Fill shape" 34 | msgstr "" 35 | 36 | #: res/swappy.glade:672 37 | msgid "Toggle shape filling" 38 | msgstr "" 39 | 40 | #: res/swappy.glade:684 41 | msgid "Transparent" 42 | msgstr "" 43 | 44 | #: res/swappy.glade:689 45 | msgid "Toggle transparency" 46 | msgstr "" 47 | 48 | #: res/swappy.glade:763 49 | msgid "Toggle Paint Panel" 50 | msgstr "" 51 | 52 | #: res/swappy.glade:789 53 | msgid "Undo Last Paint" 54 | msgstr "" 55 | 56 | #: res/swappy.glade:808 57 | msgid "Redo Previous Paint" 58 | msgstr "" 59 | 60 | #: res/swappy.glade:827 61 | msgid "Clear Paints" 62 | msgstr "" 63 | 64 | #: res/swappy.glade:855 65 | msgid "Copy Surface" 66 | msgstr "" 67 | 68 | #: res/swappy.glade:871 69 | msgid "Save Surface" 70 | msgstr "" 71 | -------------------------------------------------------------------------------- /src/po/tr.po: -------------------------------------------------------------------------------- 1 | # Turkish translations for swappy package. 2 | # Copyright (C) 2020 THE swappy'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the swappy package. 4 | # Oğuz Ersen , 2020-2022. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: swappy\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n" 11 | "PO-Revision-Date: 2022-11-25 10:36+0300\n" 12 | "Last-Translator: Oğuz Ersen \n" 13 | "Language-Team: Turkish \n" 14 | "Language: tr\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=2; plural=(n != 1);\n" 19 | 20 | #: res/swappy.glade:461 21 | msgid "Line Width" 22 | msgstr "Çizgi Genişliği" 23 | 24 | #: res/swappy.glade:531 25 | msgid "Text Size" 26 | msgstr "Metin Boyutu" 27 | 28 | #: res/swappy.glade:601 29 | msgid "Transparency" 30 | msgstr "" 31 | 32 | #: res/swappy.glade:667 33 | msgid "Fill shape" 34 | msgstr "Şekli Doldur" 35 | 36 | #: res/swappy.glade:672 37 | msgid "Toggle shape filling" 38 | msgstr "Şekil Doldurmayı Aç/Kapat" 39 | 40 | #: res/swappy.glade:684 41 | msgid "Transparent" 42 | msgstr "" 43 | 44 | #: res/swappy.glade:689 45 | msgid "Toggle transparency" 46 | msgstr "" 47 | 48 | #: res/swappy.glade:763 49 | msgid "Toggle Paint Panel" 50 | msgstr "Boyama Panelini Aç/Kapat" 51 | 52 | #: res/swappy.glade:789 53 | msgid "Undo Last Paint" 54 | msgstr "Son Boyamayı Geri Al" 55 | 56 | #: res/swappy.glade:808 57 | msgid "Redo Previous Paint" 58 | msgstr "Önceki Boyamayı Tekrarla" 59 | 60 | #: res/swappy.glade:827 61 | msgid "Clear Paints" 62 | msgstr "Boyamaları Temizle" 63 | 64 | #: res/swappy.glade:855 65 | msgid "Copy Surface" 66 | msgstr "Yüzeyi Kopyala" 67 | 68 | #: res/swappy.glade:871 69 | msgid "Save Surface" 70 | msgstr "Yüzeyi Kaydet" 71 | -------------------------------------------------------------------------------- /src/po/zh_CN.po: -------------------------------------------------------------------------------- 1 | # English translations for swappy package. 2 | # Copyright (C) 2020 THE swappy'S COPYRIGHT HOLDER 3 | # This file is distributed under the same license as the swappy package. 4 | # Automatically generated, 2020. 5 | # 6 | msgid "" 7 | msgstr "" 8 | "Project-Id-Version: swappy\n" 9 | "Report-Msgid-Bugs-To: \n" 10 | "POT-Creation-Date: 2024-11-06 23:00+0100\n" 11 | "PO-Revision-Date: 2020-06-21 21:57-0400\n" 12 | "Last-Translator: Automatically generated\n" 13 | "Language-Team: none\n" 14 | "Language: zh_CN\n" 15 | "MIME-Version: 1.0\n" 16 | "Content-Type: text/plain; charset=UTF-8\n" 17 | "Content-Transfer-Encoding: 8bit\n" 18 | "Plural-Forms: nplurals=1; plural=0;\n" 19 | 20 | #: res/swappy.glade:461 21 | msgid "Line Width" 22 | msgstr "行宽" 23 | 24 | #: res/swappy.glade:531 25 | msgid "Text Size" 26 | msgstr "文本大小" 27 | 28 | #: res/swappy.glade:601 29 | msgid "Transparency" 30 | msgstr "" 31 | 32 | #: res/swappy.glade:667 33 | msgid "Fill shape" 34 | msgstr "填充" 35 | 36 | #: res/swappy.glade:672 37 | msgid "Toggle shape filling" 38 | msgstr "切换填充状态" 39 | 40 | #: res/swappy.glade:684 41 | msgid "Transparent" 42 | msgstr "" 43 | 44 | #: res/swappy.glade:689 45 | msgid "Toggle transparency" 46 | msgstr "" 47 | 48 | #: res/swappy.glade:763 49 | msgid "Toggle Paint Panel" 50 | msgstr "切换绘图板状态" 51 | 52 | #: res/swappy.glade:789 53 | msgid "Undo Last Paint" 54 | msgstr "撤销" 55 | 56 | #: res/swappy.glade:808 57 | msgid "Redo Previous Paint" 58 | msgstr "恢复" 59 | 60 | #: res/swappy.glade:827 61 | msgid "Clear Paints" 62 | msgstr "清除绘图" 63 | 64 | #: res/swappy.glade:855 65 | msgid "Copy Surface" 66 | msgstr "复制" 67 | 68 | #: res/swappy.glade:871 69 | msgid "Save Surface" 70 | msgstr "保存" 71 | -------------------------------------------------------------------------------- /src/render.c: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include 5 | 6 | #include "algebra.h" 7 | #include "swappy.h" 8 | #include "util.h" 9 | 10 | #define pango_layout_t PangoLayout 11 | #define pango_font_description_t PangoFontDescription 12 | #define pango_rectangle_t PangoRectangle 13 | 14 | /* 15 | * This code was largely taken from Kristian Høgsberg and Chris Wilson from: 16 | * https://www.cairographics.org/cookbook/blur.c/ 17 | */ 18 | static cairo_surface_t *blur_surface(cairo_surface_t *surface, double x, 19 | double y, double width, double height) { 20 | cairo_surface_t *dest_surface, *tmp_surface, *final = NULL; 21 | cairo_t *cr; 22 | int src_width, src_height; 23 | int src_stride, dst_stride; 24 | guint u, v, w, z; 25 | uint8_t *dst, *tmp; 26 | uint32_t *s, *d, p; 27 | int i, j, k; 28 | const int radius = 4; 29 | const double sigma = 3.1; 30 | struct gaussian_kernel *gaussian = gaussian_kernel(radius, sigma); 31 | const int size = gaussian->size; 32 | const int half = (int)radius * 2; 33 | gdouble scale_x, scale_y; 34 | guint sum, pass, nb_passes; 35 | 36 | sum = (guint)gaussian->sum; 37 | 38 | if (cairo_surface_status(surface)) { 39 | return NULL; 40 | } 41 | 42 | cairo_surface_get_device_scale(surface, &scale_x, &scale_y); 43 | 44 | cairo_format_t src_format = cairo_image_surface_get_format(surface); 45 | switch (src_format) { 46 | case CAIRO_FORMAT_A1: 47 | case CAIRO_FORMAT_A8: 48 | default: 49 | g_warning("source surface format: %d is not supported", src_format); 50 | return NULL; 51 | case CAIRO_FORMAT_RGB24: 52 | case CAIRO_FORMAT_ARGB32: 53 | break; 54 | } 55 | 56 | src_stride = cairo_image_surface_get_stride(surface); 57 | src_width = cairo_image_surface_get_width(surface); 58 | src_height = cairo_image_surface_get_height(surface); 59 | 60 | g_assert(src_height >= height); 61 | g_assert(src_width >= width); 62 | 63 | dest_surface = cairo_image_surface_create(src_format, src_width, src_height); 64 | tmp_surface = cairo_image_surface_create(src_format, src_width, src_height); 65 | 66 | cairo_surface_set_device_scale(dest_surface, scale_x, scale_y); 67 | cairo_surface_set_device_scale(tmp_surface, scale_x, scale_y); 68 | 69 | if (cairo_surface_status(dest_surface) || cairo_surface_status(tmp_surface)) { 70 | goto cleanup; 71 | } 72 | 73 | cr = cairo_create(tmp_surface); 74 | cairo_set_source_surface(cr, surface, 0, 0); 75 | cairo_paint(cr); 76 | cairo_destroy(cr); 77 | 78 | cr = cairo_create(dest_surface); 79 | cairo_set_source_surface(cr, surface, 0, 0); 80 | cairo_paint(cr); 81 | cairo_destroy(cr); 82 | 83 | dst = cairo_image_surface_get_data(dest_surface); 84 | tmp = cairo_image_surface_get_data(tmp_surface); 85 | dst_stride = cairo_image_surface_get_stride(dest_surface); 86 | 87 | nb_passes = (guint)sqrt(scale_x * scale_y) + 1; 88 | 89 | int start_x = CLAMP(x * scale_x, 0, src_width); 90 | int start_y = CLAMP(y * scale_y, 0, src_height); 91 | 92 | int end_x = CLAMP((x + width) * scale_x, 0, src_width); 93 | int end_y = CLAMP((y + height) * scale_y, 0, src_height); 94 | 95 | for (pass = 0; pass < nb_passes; pass++) { 96 | /* Horizontally blur from dst -> tmp */ 97 | for (i = start_y; i < end_y; i++) { 98 | s = (uint32_t *)(dst + i * src_stride); 99 | d = (uint32_t *)(tmp + i * dst_stride); 100 | for (j = start_x; j < end_x; j++) { 101 | u = v = w = z = 0; 102 | for (k = 0; k < size; k++) { 103 | gdouble multiplier = gaussian->kernel[k]; 104 | 105 | if (j - half + k < 0 || j - half + k >= src_width) { 106 | continue; 107 | } 108 | 109 | p = s[j - half + k]; 110 | 111 | u += ((p >> 24) & 0xff) * multiplier; 112 | v += ((p >> 16) & 0xff) * multiplier; 113 | w += ((p >> 8) & 0xff) * multiplier; 114 | z += ((p >> 0) & 0xff) * multiplier; 115 | } 116 | 117 | d[j] = (u / sum << 24) | (v / sum << 16) | (w / sum << 8) | z / sum; 118 | } 119 | } 120 | 121 | /* Then vertically blur from tmp -> dst */ 122 | for (i = start_y; i < end_y; i++) { 123 | d = (uint32_t *)(dst + i * dst_stride); 124 | for (j = start_x; j < end_x; j++) { 125 | u = v = w = z = 0; 126 | for (k = 0; k < size; k++) { 127 | gdouble multiplier = gaussian->kernel[k]; 128 | 129 | if (i - half + k < 0 || i - half + k >= src_height) { 130 | continue; 131 | } 132 | 133 | s = (uint32_t *)(tmp + (i - half + k) * dst_stride); 134 | p = s[j]; 135 | 136 | u += ((p >> 24) & 0xff) * multiplier; 137 | v += ((p >> 16) & 0xff) * multiplier; 138 | w += ((p >> 8) & 0xff) * multiplier; 139 | z += ((p >> 0) & 0xff) * multiplier; 140 | } 141 | 142 | d[j] = (u / sum << 24) | (v / sum << 16) | (w / sum << 8) | z / sum; 143 | } 144 | } 145 | } 146 | 147 | // Mark destination surface as dirty since it was altered with custom data. 148 | cairo_surface_mark_dirty(dest_surface); 149 | 150 | final = cairo_image_surface_create(src_format, (int)(width * scale_x), 151 | (int)(height * scale_y)); 152 | 153 | if (cairo_surface_status(final)) { 154 | goto cleanup; 155 | } 156 | 157 | cairo_surface_set_device_scale(final, scale_x, scale_y); 158 | cr = cairo_create(final); 159 | cairo_set_source_surface(cr, dest_surface, -x, -y); 160 | cairo_paint(cr); 161 | cairo_destroy(cr); 162 | 163 | cleanup: 164 | cairo_surface_destroy(dest_surface); 165 | cairo_surface_destroy(tmp_surface); 166 | gaussian_kernel_free(gaussian); 167 | return final; 168 | } 169 | 170 | static void convert_pango_rectangle_to_swappy_box(pango_rectangle_t rectangle, 171 | struct swappy_box *box) { 172 | if (!box) { 173 | return; 174 | } 175 | 176 | box->x = pango_units_to_double(rectangle.x); 177 | box->y = pango_units_to_double(rectangle.y); 178 | box->width = pango_units_to_double(rectangle.width); 179 | box->height = pango_units_to_double(rectangle.height); 180 | } 181 | 182 | static void render_text(cairo_t *cr, struct swappy_paint_text text, 183 | struct swappy_state *state) { 184 | char pango_font[255]; 185 | double x = fmin(text.from.x, text.to.x); 186 | double y = fmin(text.from.y, text.to.y); 187 | double w = fabs(text.from.x - text.to.x); 188 | double h = fabs(text.from.y - text.to.y); 189 | 190 | cairo_surface_t *surface = 191 | cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); 192 | cairo_t *crt = cairo_create(surface); 193 | 194 | pango_layout_t *layout = pango_cairo_create_layout(crt); 195 | pango_layout_set_text(layout, text.text, -1); 196 | g_snprintf(pango_font, 255, "%s %d", text.font, (int)text.s); 197 | pango_font_description_t *desc = 198 | pango_font_description_from_string(pango_font); 199 | pango_layout_set_width(layout, pango_units_from_double(w)); 200 | pango_layout_set_font_description(layout, desc); 201 | pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); 202 | pango_font_description_free(desc); 203 | 204 | if (text.mode == SWAPPY_TEXT_MODE_EDIT) { 205 | pango_rectangle_t strong_pos; 206 | struct swappy_box cursor_box; 207 | cairo_set_source_rgba(cr, 0.5, 0.5, 0.5, 0.3); 208 | cairo_set_line_width(cr, 5); 209 | cairo_rectangle(cr, x, y, w, h); 210 | cairo_stroke(cr); 211 | glong bytes_til_cursor = string_get_nb_bytes_until(text.text, text.cursor); 212 | pango_layout_get_cursor_pos(layout, bytes_til_cursor, &strong_pos, NULL); 213 | convert_pango_rectangle_to_swappy_box(strong_pos, &cursor_box); 214 | cairo_move_to(crt, cursor_box.x, cursor_box.y); 215 | cairo_set_source_rgba(crt, 0.3, 0.3, 0.3, 1); 216 | cairo_line_to(crt, cursor_box.x, cursor_box.y + cursor_box.height); 217 | cairo_stroke(crt); 218 | GdkRectangle area = {x + cursor_box.x, y + cursor_box.y + cursor_box.height, 219 | 0, 0}; 220 | gtk_im_context_set_cursor_location(state->ui->im_context, &area); 221 | } 222 | 223 | cairo_rectangle(crt, 0, 0, w, h); 224 | cairo_set_source_rgba(crt, text.r, text.g, text.b, text.a); 225 | cairo_move_to(crt, 0, 0); 226 | pango_cairo_show_layout(crt, layout); 227 | 228 | cairo_set_source_surface(cr, surface, x, y); 229 | cairo_paint(cr); 230 | 231 | cairo_destroy(crt); 232 | cairo_surface_destroy(surface); 233 | g_object_unref(layout); 234 | } 235 | 236 | static void render_shape_arrow(cairo_t *cr, struct swappy_paint_shape shape) { 237 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a); 238 | cairo_set_line_width(cr, shape.w); 239 | 240 | double ftx = shape.to.x - shape.from.x; 241 | double fty = shape.to.y - shape.from.y; 242 | double ftn = sqrt(ftx * ftx + fty * fty); 243 | 244 | double r = 20; 245 | double scaling_factor = shape.w / 4; 246 | 247 | double alpha = G_PI / 6; 248 | double ta = 5 * alpha; 249 | double tb = 7 * alpha; 250 | double xa = r * cos(ta); 251 | double ya = r * sin(ta); 252 | double xb = r * cos(tb); 253 | double yb = r * sin(tb); 254 | double xc = ftn - fabs(xa) * scaling_factor; 255 | 256 | if (xc < DBL_EPSILON) { 257 | xc = 0; 258 | } 259 | 260 | if (ftn < DBL_EPSILON) { 261 | return; 262 | } 263 | 264 | double theta = copysign(1.0, fty) * acos(ftx / ftn); 265 | 266 | // Draw line 267 | cairo_save(cr); 268 | cairo_translate(cr, shape.from.x, shape.from.y); 269 | cairo_rotate(cr, theta); 270 | cairo_move_to(cr, 0, 0); 271 | cairo_line_to(cr, xc, 0); 272 | cairo_stroke(cr); 273 | cairo_restore(cr); 274 | 275 | // Draw arrow 276 | cairo_save(cr); 277 | cairo_translate(cr, shape.to.x, shape.to.y); 278 | cairo_rotate(cr, theta); 279 | cairo_scale(cr, scaling_factor, scaling_factor); 280 | cairo_move_to(cr, 0, 0); 281 | cairo_line_to(cr, xa, ya); 282 | cairo_line_to(cr, xb, yb); 283 | cairo_line_to(cr, 0, 0); 284 | cairo_fill(cr); 285 | cairo_restore(cr); 286 | } 287 | 288 | static void render_shape_ellipse(cairo_t *cr, struct swappy_paint_shape shape) { 289 | double x = fabs(shape.from.x - shape.to.x); 290 | double y = fabs(shape.from.y - shape.to.y); 291 | 292 | double n = sqrt(x * x + y * y); 293 | 294 | double xc, yc, r; 295 | 296 | if (shape.should_center_at_from) { 297 | xc = shape.from.x; 298 | yc = shape.from.y; 299 | 300 | r = n; 301 | } else { 302 | xc = shape.from.x + ((shape.to.x - shape.from.x) / 2); 303 | yc = shape.from.y + ((shape.to.y - shape.from.y) / 2); 304 | 305 | r = n / 2; 306 | } 307 | 308 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a); 309 | cairo_set_line_width(cr, shape.w); 310 | 311 | cairo_matrix_t save_matrix; 312 | cairo_get_matrix(cr, &save_matrix); 313 | cairo_translate(cr, xc, yc); 314 | cairo_scale(cr, x / n, y / n); 315 | cairo_arc(cr, 0, 0, r, 0, 2 * G_PI); 316 | cairo_set_matrix(cr, &save_matrix); 317 | 318 | switch (shape.operation) { 319 | case SWAPPY_PAINT_SHAPE_OPERATION_STROKE: 320 | cairo_stroke(cr); 321 | break; 322 | case SWAPPY_PAINT_SHAPE_OPERATION_FILL: 323 | cairo_fill(cr); 324 | break; 325 | default: 326 | cairo_stroke(cr); 327 | break; 328 | } 329 | 330 | cairo_close_path(cr); 331 | } 332 | 333 | static void render_shape_rectangle(cairo_t *cr, 334 | struct swappy_paint_shape shape) { 335 | double x, y, w, h; 336 | 337 | if (shape.should_center_at_from) { 338 | x = shape.from.x - fabs(shape.from.x - shape.to.x); 339 | y = shape.from.y - fabs(shape.from.y - shape.to.y); 340 | w = fabs(shape.from.x - shape.to.x) * 2; 341 | h = fabs(shape.from.y - shape.to.y) * 2; 342 | } else { 343 | x = fmin(shape.from.x, shape.to.x); 344 | y = fmin(shape.from.y, shape.to.y); 345 | w = fabs(shape.from.x - shape.to.x); 346 | h = fabs(shape.from.y - shape.to.y); 347 | } 348 | 349 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a); 350 | cairo_set_line_width(cr, shape.w); 351 | 352 | cairo_rectangle(cr, x, y, w, h); 353 | cairo_close_path(cr); 354 | 355 | switch (shape.operation) { 356 | case SWAPPY_PAINT_SHAPE_OPERATION_STROKE: 357 | cairo_stroke(cr); 358 | break; 359 | case SWAPPY_PAINT_SHAPE_OPERATION_FILL: 360 | cairo_fill(cr); 361 | break; 362 | default: 363 | cairo_stroke(cr); 364 | break; 365 | } 366 | } 367 | 368 | static void render_shape(cairo_t *cr, struct swappy_paint_shape shape) { 369 | cairo_save(cr); 370 | switch (shape.type) { 371 | case SWAPPY_PAINT_MODE_RECTANGLE: 372 | render_shape_rectangle(cr, shape); 373 | break; 374 | case SWAPPY_PAINT_MODE_ELLIPSE: 375 | render_shape_ellipse(cr, shape); 376 | break; 377 | case SWAPPY_PAINT_MODE_ARROW: 378 | render_shape_arrow(cr, shape); 379 | break; 380 | default: 381 | break; 382 | } 383 | cairo_restore(cr); 384 | } 385 | 386 | static void clear_surface(cairo_t *cr) { 387 | cairo_save(cr); 388 | cairo_set_source_rgba(cr, 0, 0, 0, 0); 389 | cairo_set_operator(cr, CAIRO_OPERATOR_SOURCE); 390 | cairo_paint(cr); 391 | cairo_restore(cr); 392 | } 393 | 394 | static void render_blur(cairo_t *cr, struct swappy_paint *paint) { 395 | struct swappy_paint_blur blur = paint->content.blur; 396 | 397 | cairo_surface_t *target = cairo_get_target(cr); 398 | 399 | double x = MIN(blur.from.x, blur.to.x); 400 | double y = MIN(blur.from.y, blur.to.y); 401 | double w = ABS(blur.from.x - blur.to.x); 402 | double h = ABS(blur.from.y - blur.to.y); 403 | 404 | cairo_save(cr); 405 | 406 | if (paint->is_committed) { 407 | // Surface has already been blurred, reuse it in future passes 408 | if (blur.surface) { 409 | cairo_surface_t *surface = blur.surface; 410 | if (surface && cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) { 411 | cairo_set_source_surface(cr, surface, x, y); 412 | cairo_paint(cr); 413 | } 414 | } else { 415 | // Blur surface and reuse it in future passes 416 | g_info( 417 | "blurring surface on following image coordinates: %.2lf,%.2lf size: " 418 | "%.2lfx%.2lf", 419 | x, y, w, h); 420 | cairo_surface_t *blurred = blur_surface(target, x, y, w, h); 421 | 422 | if (blurred && cairo_surface_status(blurred) == CAIRO_STATUS_SUCCESS) { 423 | cairo_set_source_surface(cr, blurred, x, y); 424 | cairo_paint(cr); 425 | paint->content.blur.surface = blurred; 426 | } 427 | } 428 | } else { 429 | // Blur not committed yet, draw bounding rectangle 430 | struct swappy_paint_shape rect = { 431 | .r = 0, 432 | .g = 0.5, 433 | .b = 1, 434 | .a = 0.5, 435 | .w = 5, 436 | .from = blur.from, 437 | .to = blur.to, 438 | .type = SWAPPY_PAINT_MODE_RECTANGLE, 439 | .operation = SWAPPY_PAINT_SHAPE_OPERATION_FILL, 440 | }; 441 | render_shape_rectangle(cr, rect); 442 | } 443 | 444 | cairo_restore(cr); 445 | } 446 | 447 | static void render_brush(cairo_t *cr, struct swappy_paint_brush brush) { 448 | cairo_set_source_rgba(cr, brush.r, brush.g, brush.b, brush.a); 449 | cairo_set_line_width(cr, brush.w); 450 | cairo_set_line_join(cr, CAIRO_LINE_JOIN_BEVEL); 451 | 452 | guint l = g_list_length(brush.points); 453 | 454 | if (l == 1) { 455 | struct swappy_point *point = g_list_nth_data(brush.points, 0); 456 | cairo_rectangle(cr, point->x, point->y, brush.w, brush.w); 457 | cairo_fill(cr); 458 | } else { 459 | for (GList *elem = brush.points; elem; elem = elem->next) { 460 | struct swappy_point *point = elem->data; 461 | cairo_line_to(cr, point->x, point->y); 462 | } 463 | cairo_stroke(cr); 464 | } 465 | } 466 | 467 | static void render_image(cairo_t *cr, struct swappy_state *state) { 468 | cairo_surface_t *surface = state->original_image_surface; 469 | 470 | cairo_save(cr); 471 | 472 | if (surface && !cairo_surface_status(surface)) { 473 | cairo_set_source_surface(cr, surface, 0, 0); 474 | cairo_paint(cr); 475 | } 476 | 477 | cairo_restore(cr); 478 | } 479 | 480 | static void render_paint(cairo_t *cr, struct swappy_paint *paint, 481 | struct swappy_state *state) { 482 | if (!paint->can_draw) { 483 | return; 484 | } 485 | switch (paint->type) { 486 | case SWAPPY_PAINT_MODE_BLUR: 487 | render_blur(cr, paint); 488 | break; 489 | case SWAPPY_PAINT_MODE_BRUSH: 490 | render_brush(cr, paint->content.brush); 491 | break; 492 | case SWAPPY_PAINT_MODE_RECTANGLE: 493 | case SWAPPY_PAINT_MODE_ELLIPSE: 494 | case SWAPPY_PAINT_MODE_ARROW: 495 | render_shape(cr, paint->content.shape); 496 | break; 497 | case SWAPPY_PAINT_MODE_TEXT: 498 | render_text(cr, paint->content.text, state); 499 | break; 500 | default: 501 | g_info("unable to render paint with type: %d", paint->type); 502 | break; 503 | } 504 | } 505 | 506 | static void render_paints(cairo_t *cr, struct swappy_state *state) { 507 | for (GList *elem = g_list_last(state->paints); elem; elem = elem->prev) { 508 | struct swappy_paint *paint = elem->data; 509 | render_paint(cr, paint, state); 510 | } 511 | 512 | if (state->temp_paint) { 513 | render_paint(cr, state->temp_paint, state); 514 | } 515 | } 516 | 517 | void render_state(struct swappy_state *state) { 518 | cairo_surface_t *surface = state->rendering_surface; 519 | cairo_t *cr = cairo_create(surface); 520 | 521 | clear_surface(cr); 522 | render_image(cr, state); 523 | render_paints(cr, state); 524 | 525 | cairo_destroy(cr); 526 | 527 | // Drawing is finished, notify the GtkDrawingArea it needs to be redrawn. 528 | gtk_widget_queue_draw(state->ui->area); 529 | } 530 | -------------------------------------------------------------------------------- /src/util.c: -------------------------------------------------------------------------------- 1 | #include "util.h" 2 | 3 | #include 4 | #include 5 | 6 | gchar *string_remove_at(gchar *str, glong pos) { 7 | glong str_len = strlen(str); 8 | glong ustr_len = g_utf8_strlen(str, -1); 9 | gchar *new_str = g_new0(gchar, MAX(str_len, 1)); 10 | gchar *buffer_source = str; 11 | gchar *buffer_copy = new_str; 12 | glong i = 0; 13 | gint bytes; 14 | gunichar c; 15 | 16 | if (pos <= ustr_len && g_utf8_validate(str, -1, NULL)) { 17 | while (*buffer_source != '\0') { 18 | c = g_utf8_get_char(buffer_source); 19 | buffer_source = g_utf8_next_char(buffer_source); 20 | if (i != pos) { 21 | bytes = g_unichar_to_utf8(c, buffer_copy); 22 | buffer_copy += bytes; 23 | } 24 | i++; 25 | } 26 | } 27 | 28 | return new_str; 29 | } 30 | 31 | gchar *string_insert_chars_at(gchar *str, gchar *chars, glong pos) { 32 | gchar *new_str = NULL; 33 | 34 | if (g_utf8_validate(str, -1, NULL) && g_utf8_validate(chars, -1, NULL) && 35 | pos >= 0 && pos <= g_utf8_strlen(str, -1)) { 36 | gchar *from = g_utf8_substring(str, 0, pos); 37 | gchar *end = g_utf8_offset_to_pointer(str, pos); 38 | 39 | new_str = g_strconcat(from, chars, end, NULL); 40 | 41 | g_free(from); 42 | 43 | } else { 44 | new_str = g_new0(gchar, 1); 45 | } 46 | 47 | return new_str; 48 | } 49 | 50 | glong string_get_nb_bytes_until(gchar *str, glong until) { 51 | glong ret = 0; 52 | if (str) { 53 | gchar *sub = g_utf8_substring(str, 0, until); 54 | ret = strlen(sub); 55 | g_free(sub); 56 | } 57 | 58 | return ret; 59 | } 60 | 61 | void pixel_data_print(guint32 pixel) { 62 | const guint32 r = pixel >> 24 & 0xff; 63 | const guint32 g = pixel >> 16 & 0xff; 64 | const guint32 b = pixel >> 8 & 0xff; 65 | const guint32 a = pixel >> 0 & 0xff; 66 | 67 | g_debug("rgba(%u, %d, %u, %u)", r, g, b, a); 68 | } 69 | -------------------------------------------------------------------------------- /swappy.1.scd: -------------------------------------------------------------------------------- 1 | swappy(1) 2 | 3 | # NAME 4 | 5 | swappy - grab and edit on the fly snapshots of a Wayland compositor 6 | 7 | # SYNOPSIS 8 | 9 | *swappy* [options...] 10 | 11 | # DESCRIPTION 12 | 13 | swappy is a command-line utility to take and edit screenshots of Wayland 14 | desktops. Works great with grim, slurp and sway. But can easily work with 15 | other screen copy tools that can output a final image to *stdout*. 16 | 17 | swappy will save the annotated images to the config *save_dir*, see below. 18 | 19 | If absent, then if it will try to default to a *Desktop* folder following this 20 | pattern: *$XDG\_DESKTOP\_DIR*. If this variable is not set, it will revert to: 21 | *$XDG\_CONFIG\_HOME/Desktop*. If *$XDG\_CONFIG\_HOME* is not set, it will revert 22 | to: *$HOME/Desktop*. 23 | 24 | # OPTIONS 25 | 26 | *-h, --help* 27 | Show help message and quit. 28 | 29 | *-v, --version* 30 | Show version and quit. 31 | 32 | *-f, --file* 33 | An image file to load for editing. 34 | 35 | If set to *-*, read the file from standard input instead. This is grim 36 | friendly. 37 | 38 | *-o, --output-file * 39 | Print the final surface to ** when exiting the application. 40 | 41 | If set to *-*, prints the final surface to *stdout*. 42 | 43 | Note that the *Save* button will save the image to the config *save_dir* 44 | parameter, as described in the DESCRIPTION section. 45 | 46 | # CONFIG FILE 47 | 48 | The config file is located at *$XDG\_CONFIG\_HOME/swappy/config* or at 49 | *$HOME/.config/swappy/config*. The file follows the GLib *conf* format. 50 | 51 | ``` 52 | [Section] 53 | key=value 54 | ``` 55 | 56 | The following lines can be used as swappy's default: 57 | 58 | ``` 59 | [Default] 60 | save_dir=$HOME/Desktop 61 | save_filename_format=swappy-%Y%m%d-%H%M%S.png 62 | show_panel=false 63 | line_size=5 64 | text_size=20 65 | text_font=sans-serif 66 | paint_mode=brush 67 | early_exit=false 68 | fill_shape=false 69 | auto_save=false 70 | custom_color=rgba(192,125,17,1) 71 | transparent=false 72 | transparency=50 73 | ``` 74 | 75 | - *save_dir* is where swappshots will be saved, can contain env variables, when it does not exist, swappy attempts to create it first, but does not abort if directory creation fails 76 | - *save_filename_format* is the filename template, if it contains a date format, this will be parsed into a timestamp. Format is detailed in strftime(3). If this date format is missing, filename will have no timestamp 77 | - *show_panel* is used to toggle the paint panel on or off upon startup 78 | - *line_size* is the default line size (must be between 1 and 50) 79 | - *text_size* is the default text size (must be between 10 and 50) 80 | - *text_font* is the font used to render text, its format is pango friendly 81 | - *paint_mode* is the mode activated at application start (must be one of: brush|text|rectangle|ellipse|arrow|blur, matching is case-insensitive) 82 | - *early_exit* is used to make the application exit after saving the picture or copying it to the clipboard 83 | - *fill_shape* is used to toggle shape filling (for the rectangle and ellipsis tools) on or off upon startup 84 | - *auto_save* is used to toggle auto saving of final buffer to *save_dir* upon exit 85 | - *custom_color* is used to set a default value for the custom color. Accepted 86 | formats are: standard name (one of: https://github.com/rgb-x/system/blob/master/root/etc/X11/rgb.txt), #rgb, #rrggbb, #rrrgggbbb, #rrrrggggbbbb, rgb(r,b,g), rgba(r,g,b,a) 87 | - *transparency* is used to set transparency of everything that is drawn during startup 88 | - *transparent* is used to toggle transparency during startup 89 | 90 | 91 | # KEY BINDINGS 92 | 93 | ## LAYOUT 94 | 95 | - *Ctrl+b*: Toggle Paint Panel 96 | 97 | ## PAINT MODE 98 | 99 | - *b*: Switch to Brush 100 | - `e` `t`: Switch to Text (Editor) 101 | - `r` `s`: Switch to Rectangle (Square) 102 | - `c` `o`: Switch to Ellipse (Circle) 103 | - *a*: Switch to Arrow 104 | - *d*: Switch to Blur (d stands for droplet) 105 | 106 | - *R*: Use Red Color 107 | - *G*: Use Green Color 108 | - *B*: Use Blue Color 109 | - *C*: Use Custom Color 110 | - *Minus*: Reduce Stroke Size 111 | - *Plus*: Increase Stroke Size 112 | - *Equal*: Reset Stroke Size 113 | - *f*: Toggle Shape Filling 114 | - *T*: Toggle Transparency 115 | - `x` `k`: Clear Paints (cannot be undone) 116 | 117 | ## MODIFIERS 118 | 119 | - *Ctrl*: Center Shape (Rectangle & Ellipse) based on draw start 120 | 121 | ## HEADER BAR 122 | 123 | - *Ctrl+z*: Undo 124 | - *Ctrl+Shift+z* or *Ctrl+y*: Redo 125 | - *Ctrl+s*: Save to file (see man page) 126 | - *Ctrl+c*: Copy to clipboard 127 | - *Escape* or *q* or *Ctrl+w*: Quit swappy 128 | 129 | # AUTHORS 130 | 131 | Written and maintained by jtheoof . See 132 | https://github.com/jtheoof/swappy. 133 | -------------------------------------------------------------------------------- /test/images/heart-transparent.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/test/images/heart-transparent.png -------------------------------------------------------------------------------- /test/images/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/test/images/large.png -------------------------------------------------------------------------------- /test/images/passwords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/test/images/passwords.png -------------------------------------------------------------------------------- /test/images/small-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/c25040258fb9dde3dd7313e419a514436741cfe5/test/images/small-blue.png --------------------------------------------------------------------------------