├── .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 ├── 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 | AnalyzeTemporaryDtors: false 6 | FormatStyle: none 7 | User: jattali 8 | CheckOptions: 9 | - key: cert-dcl16-c.NewSuffixes 10 | value: 'L;LL;LU;LLU' 11 | - key: cert-oop54-cpp.WarnOnlyIfThisHasSuspiciousField 12 | value: '0' 13 | - key: cppcoreguidelines-explicit-virtual-functions.IgnoreDestructors 14 | value: '1' 15 | - key: cppcoreguidelines-non-private-member-variables-in-classes.IgnoreClassesWithAllMemberVariablesBeingPublic 16 | value: '1' 17 | - key: google-readability-braces-around-statements.ShortStatementLines 18 | value: '1' 19 | - key: google-readability-function-size.StatementThreshold 20 | value: '800' 21 | - key: google-readability-namespace-comments.ShortNamespaceLines 22 | value: '10' 23 | - key: google-readability-namespace-comments.SpacesBeforeComments 24 | value: '2' 25 | - key: modernize-loop-convert.MaxCopySize 26 | value: '16' 27 | - key: modernize-loop-convert.MinConfidence 28 | value: reasonable 29 | - key: modernize-loop-convert.NamingStyle 30 | value: CamelCase 31 | - key: modernize-pass-by-value.IncludeStyle 32 | value: llvm 33 | - key: modernize-replace-auto-ptr.IncludeStyle 34 | value: llvm 35 | - key: modernize-use-nullptr.NullMacros 36 | value: 'NULL' 37 | ... 38 | 39 | -------------------------------------------------------------------------------- /.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-gcc: 7 | runs-on: ubuntu-20.04 8 | steps: 9 | - uses: actions/checkout@v1 10 | - name: GCC 11 | run: | 12 | sudo apt-get update 13 | sudo apt --yes install libgtk-3-dev meson ninja-build scdoc 14 | pkg-config --list-all 15 | CC=gcc meson setup build 16 | ninja -C build 17 | 18 | build-clang: 19 | runs-on: ubuntu-20.04 20 | steps: 21 | - uses: actions/checkout@v1 22 | - name: Clang 23 | run: | 24 | sudo apt-get update 25 | sudo apt --yes install libgtk-3-dev meson ninja-build scdoc clang clang-format clang-tidy 26 | CC=clang meson setup build 27 | ninja -C build 28 | echo "Making sure clang-format is correct..." 29 | git ls-files -- '*.[ch]' | xargs clang-format -Werror -n 30 | echo "Running clang-tidy..." 31 | run-clang-tidy -p build 32 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | on: [pull_request] 3 | 4 | jobs: 5 | commitlint: 6 | runs-on: ubuntu-20.04 7 | name: "Commit" 8 | env: 9 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 10 | steps: 11 | - uses: actions/checkout@v2 12 | with: 13 | fetch-depth: 0 14 | - uses: wagoid/commitlint-github-action@v1 15 | with: 16 | configFile: "./.commitlintrc.yml" 17 | firstParent: false 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 | -------------------------------------------------------------------------------- /.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.5.1](https://github.com/jtheoof/swappy/compare/v1.5.0...v1.5.1) (2022-11-20) 6 | 7 | 8 | ### Bug Fixes 9 | 10 | * **ui:** use *-symbolic variant of toolbar icons ([5dc44f8](https://github.com/jtheoof/swappy/commit/5dc44f8970b0f6cdf21466bc2689ec2aa93a4385)), closes [#34](https://github.com/jtheoof/swappy/issues/34) 11 | 12 | ## [1.5.0](https://github.com/jtheoof/swappy/compare/v1.4.0...v1.5.0) (2022-11-18) 13 | 14 | 15 | ### Features 16 | 17 | * **config:** add early_exit option ([60da549](https://github.com/jtheoof/swappy/commit/60da5491e243c9edd85f6225326a68ae5e3edfd5)) 18 | * **config:** allow paint_mode to be configured through config file ([2f35f02](https://github.com/jtheoof/swappy/commit/2f35f02b4e89bf67b6e9cc461e874331d8ce2a4c)) 19 | * **config:** try to create `save_dir` if it does not exist ([4fb291a](https://github.com/jtheoof/swappy/commit/4fb291ad4b0b116afeaa7094b040083111b74674)) 20 | * **ui:** allow filling rectangles and ellipsis ([8ee55f7](https://github.com/jtheoof/swappy/commit/8ee55f7d52ce6ac71752981863f5795fef460049)), closes [#120](https://github.com/jtheoof/swappy/issues/120) 21 | 22 | ## [1.4.0](https://github.com/jtheoof/swappy/compare/v1.3.1...v1.4.0) (2021-09-06) 23 | 24 | 25 | ### Features 26 | 27 | * **draw:** draw shape from center if holding control ([d80c361](https://github.com/jtheoof/swappy/commit/d80c3614895d3b5da479831c651cc1afa2fcf916)) 28 | * **i18n:** add french translations ([cacb283](https://github.com/jtheoof/swappy/commit/cacb2830e4cc41010d6ab96655054d2eb1651651)) 29 | 30 | 31 | ### Bug Fixes 32 | 33 | * **desktop:** remove annotation from desktop categories ([0d383f6](https://github.com/jtheoof/swappy/commit/0d383f690b99026c340eab1efa590c48d54e7368)) 34 | * **desktop:** various fixes ([42425c0](https://github.com/jtheoof/swappy/commit/42425c0657a65b3f66ba4f64b1727c8198a70684)) 35 | * **i18n:** add german translations to desktop file ([c6b09e5](https://github.com/jtheoof/swappy/commit/c6b09e56399369b14a8de090a2239350dbe4aca8)) 36 | * **i18n:** add turkish translation to desktop file ([fa5769e](https://github.com/jtheoof/swappy/commit/fa5769e9406b8ab1b67aca3bff2656850362491e)) 37 | * **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) 38 | * **pixbuf:** handle invalid input file ([cdbd06d](https://github.com/jtheoof/swappy/commit/cdbd06d7af94b4aedfc2bda2231da8853f775f3a)) 39 | * **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) 40 | * **po:** update GETTEXT_PACKAGE value with project name ([7fd552e](https://github.com/jtheoof/swappy/commit/7fd552e8c41f29711212d7f70edf61ac6ada7a7d)) 41 | * **release:** properly check sha256 remote content ([91985c7](https://github.com/jtheoof/swappy/commit/91985c7994764f52c8e9d864db8ec9cf2eb1df5c)), closes [#90](https://github.com/jtheoof/swappy/issues/90) 42 | 43 | ### [1.3.1](https://github.com/jtheoof/swappy/compare/v1.3.0...v1.3.1) (2021-02-20) 44 | 45 | ## [1.3.0](https://github.com/jtheoof/swappy/compare/v1.2.1...v1.3.0) (2021-02-18) 46 | 47 | 48 | ### Features 49 | 50 | * **cli:** add configure options for filename save ([597f005](https://github.com/jtheoof/swappy/commit/597f0055b9c6230b25a7f7a7bf3f4e14c06b1fbb)) 51 | * **i18n:** add brazilian portuguese translations ([4a0eb82](https://github.com/jtheoof/swappy/commit/4a0eb82369a0859fafdcce9d242c086cd2360a84)) 52 | * **i18n:** add german translations ([b4be847](https://github.com/jtheoof/swappy/commit/b4be8476350771454b29b9ce29c62a3337acc736)) 53 | * **i18n:** add turkish translations ([c8419da](https://github.com/jtheoof/swappy/commit/c8419da7faef14223ada6853942a6d11e2acf92f)) 54 | 55 | 56 | ### Bug Fixes 57 | 58 | * **application:** unlink temp file coming from stdin ([c24e56a](https://github.com/jtheoof/swappy/commit/c24e56a165394e60b37534287e168e5d8e69627c)), closes [#80](https://github.com/jtheoof/swappy/issues/80) 59 | * **blur:** optimize blur to only render after commit ([27fcece](https://github.com/jtheoof/swappy/commit/27fcecedaeea49aaec6acdecbc51cbd865a13363)) 60 | * **blur:** rgb24 is properly handled ([c04ed63](https://github.com/jtheoof/swappy/commit/c04ed63d26e5012215198f7b41a7f2232dac1ebe)) 61 | * **clipboard:** wl-copy mimetype should be png ([a931acb](https://github.com/jtheoof/swappy/commit/a931acb2cff615badc63294ed121aba008f32ef8)), closes [#68](https://github.com/jtheoof/swappy/issues/68) 62 | * **notification:** notification shows the image icon ([eb53e5c](https://github.com/jtheoof/swappy/commit/eb53e5c2b28717f509dd58eab6da85897c0d6d9d)) 63 | * **ui:** adjust rendering surface with proper scaling ([9b72571](https://github.com/jtheoof/swappy/commit/9b72571596f9313d4efd94a4b17da8b3733fd2de)), closes [#54](https://github.com/jtheoof/swappy/issues/54) 64 | * **ui:** commit state before copying or saving ([46e5854](https://github.com/jtheoof/swappy/commit/46e5854b3cce93a82984b19ca90e3f3337952fe2)), closes [#52](https://github.com/jtheoof/swappy/issues/52) 65 | * **ui:** compute window sizes and buffers properly ([5bcffdb](https://github.com/jtheoof/swappy/commit/5bcffdbb01cc6e56f9c0f37de899b46efe68ed4a)), closes [#56](https://github.com/jtheoof/swappy/issues/56) 66 | 67 | ### [1.2.1](https://github.com/jtheoof/swappy/compare/v1.2.0...v1.2.1) (2020-07-11) 68 | 69 | 70 | ### Bug Fixes 71 | 72 | * **text:** properly handle utf-8 chars ([717ab0c](https://github.com/jtheoof/swappy/commit/717ab0c2d1757e10bb4eef17d35ccd6a991705c4)), closes [#43](https://github.com/jtheoof/swappy/issues/43) 73 | 74 | ## [1.2.0](https://github.com/jtheoof/swappy/compare/v1.1.0...v1.2.0) (2020-07-05) 75 | 76 | 77 | ### Features 78 | 79 | * **i18n:** add translatable desktop file ([cf3d7a5](https://github.com/jtheoof/swappy/commit/cf3d7a5283a7b8c34b05996f87b608513e0830ca)), closes [#35](https://github.com/jtheoof/swappy/issues/35) 80 | * **i18n:** setup i18n for swappy ([5b3c8ad](https://github.com/jtheoof/swappy/commit/5b3c8aded8fd4f9d00aa660a24127de0e1791d7f)) 81 | 82 | ## [1.2.0](https://github.com/jtheoof/swappy/compare/v1.1.0...v1.2.0) (2020-07-05) 83 | 84 | 85 | ### Features 86 | 87 | * **i18n:** add translatable desktop file ([cf3d7a5](https://github.com/jtheoof/swappy/commit/cf3d7a5283a7b8c34b05996f87b608513e0830ca)), closes [#35](https://github.com/jtheoof/swappy/issues/35) 88 | * **i18n:** setup i18n for swappy ([5b3c8ad](https://github.com/jtheoof/swappy/commit/5b3c8aded8fd4f9d00aa660a24127de0e1791d7f)) 89 | 90 | ## [1.1.0](https://github.com/jtheoof/swappy/compare/v1.0.1...v1.1.0) (2020-06-23) 91 | 92 | 93 | ### Features 94 | 95 | * **cli:** add -v and --version flags ([e32c024](https://github.com/jtheoof/swappy/commit/e32c02454ae4ec6ac30549d5fa9e80c2b64edb72)) 96 | 97 | ### [1.0.1](https://github.com/jtheoof/swappy/compare/v1.0.0...v1.0.1) (2020-06-21) 98 | 99 | 100 | ### Bug Fixes 101 | 102 | * **cli:** stop showing -g option ([ee06d66](https://github.com/jtheoof/swappy/commit/ee06d6685f6f59ffce544b45d7b51f3f4523348b)) 103 | 104 | ## 1.0.0 (2020-06-21) 105 | 106 | 107 | ### ⚠ BREAKING CHANGES 108 | 109 | * We do no support the `-g` option anymore. 110 | 111 | This tool simply makes more sense as the output of `grim` rather than 112 | trying to be `grim`. 113 | 114 | RIP my ugly wayland code, long live maintainable code. 115 | 116 | Next stop, rust? 117 | 118 | ### Features 119 | 120 | * **ui:** life is full of colors and joy ([a8c8be3](https://github.com/jtheoof/swappy/commit/a8c8be37ca996f3e1b752bca67eee594706bc08f)) 121 | * init project ([efc3ecc](https://github.com/jtheoof/swappy/commit/efc3eccc9e21892a6b0979126a23d21d3d6a3b3d)) 122 | * **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) 123 | * **application:** update app ([ce27741](https://github.com/jtheoof/swappy/commit/ce27741017554d6606e23434273f55476bc8ae37)) 124 | * **blur:** add multiple passes logic ([f9737d7](https://github.com/jtheoof/swappy/commit/f9737d78c96a5d9f4566c94702c3ec4a41d9e219)) 125 | * **blur:** remove blur configuration ([361be6a](https://github.com/jtheoof/swappy/commit/361be6aa8085143d9fd721e4c315c6b9e6fbdfca)) 126 | * **blur:** use rect blur instead of brush ([1be7798](https://github.com/jtheoof/swappy/commit/1be7798a8bcfc494b20489e2e1f8b0245f4b5e84)), closes [#17](https://github.com/jtheoof/swappy/issues/17) 127 | * **buffer:** ability to read from stdin ([02bc464](https://github.com/jtheoof/swappy/commit/02bc46456453e8530a3c9f1289dfce7e71371945)) 128 | * **buffer:** add file image support ([f6c189c](https://github.com/jtheoof/swappy/commit/f6c189c7b7f35ca4da75abaac0bd85c3d5ce5b09)) 129 | * **clipboard:** use wl-copy if present ([51b27d7](https://github.com/jtheoof/swappy/commit/51b27d768eef7fbbdab365fa94a81af5395b0e3e)) 130 | * **config:** add show_panel config ([307f579](https://github.com/jtheoof/swappy/commit/307f57956f105d22de2d8242313517b6a79ed4e2)), closes [#12](https://github.com/jtheoof/swappy/issues/12) 131 | * **config:** have overridable defaults ([ef24851](https://github.com/jtheoof/swappy/commit/ef24851deec2d6b7f76ed0fbbcd31b54b336cae3)), closes [#1](https://github.com/jtheoof/swappy/issues/1) 132 | * **draw:** convert wl_shm_format to cairo_format ([c623939](https://github.com/jtheoof/swappy/commit/c623939e02238f053312ad6367e761aec254c6fe)) 133 | * **draw:** draw the screencopy buffer ([2344414](https://github.com/jtheoof/swappy/commit/2344414102789975e6ce425a95e8b96159cf51ba)) 134 | * **layer:** use geometry size ([290d3ca](https://github.com/jtheoof/swappy/commit/290d3ca230d32ec2ef4036bf9e32f1e711fecd84)) 135 | * **paint:** introduce text paint ([3347bf2](https://github.com/jtheoof/swappy/commit/3347bf23bf17d4c2cc8e5b9bbadd657efafb28e7)) 136 | * **screencopy:** add buffer creation through screencopy ([bff8687](https://github.com/jtheoof/swappy/commit/bff8687fc81ebb57a179b1f50300f9c0cda793e3)) 137 | * **screencopy:** introduce screencopy features ([53c9770](https://github.com/jtheoof/swappy/commit/53c977080829c7e816db1a9ec45eb432f6b7b354)) 138 | * **swappy:** copy to clipboard with CTRL+C ([b90500e](https://github.com/jtheoof/swappy/commit/b90500ed34defcb8ebc67965c4dbb5d068ee8049)) 139 | * **swappy:** introduce file option ([c56df33](https://github.com/jtheoof/swappy/commit/c56df33d1880d22372e21ef0ebf5dd8805d65a76)) 140 | * **swappy:** save to file with CTRL+S ([af0b1a1](https://github.com/jtheoof/swappy/commit/af0b1a11a21faac04f8b43c4c9ef616ab5fd2b78)) 141 | * **text:** add controls in toggle panel ([c03f628](https://github.com/jtheoof/swappy/commit/c03f628de793e170d9f62c5b786fe18891bb6fa3)) 142 | * **tool:** introduce blurring capability ([fae0aea](https://github.com/jtheoof/swappy/commit/fae0aeacab6fb28e17975097c8b4c5c7e5ad57fd)), closes [#17](https://github.com/jtheoof/swappy/issues/17) 143 | * **ui:** add binding for clear action ([2bdab68](https://github.com/jtheoof/swappy/commit/2bdab684e1eace53ad7b78414ad467d312dc10ad)) 144 | * **ui:** add binding to toggle panel ([e8d2f12](https://github.com/jtheoof/swappy/commit/e8d2f12ce1737fa19972e5c4109e1c85cc2b157e)) 145 | * **ui:** add keybindings for color change ([c5ec285](https://github.com/jtheoof/swappy/commit/c5ec285ee73ddf90df2cb571e1d6c61159605c8e)) 146 | * **ui:** add keybindings for stroke size ([562a9a6](https://github.com/jtheoof/swappy/commit/562a9a6e92201677f31de126b646c619caf33863)) 147 | * **ui:** add shortcuts for undo/redo ([d7e7f2b](https://github.com/jtheoof/swappy/commit/d7e7f2b5ffd46aa36bed6ecc6709aeb94cce64ae)) 148 | * **ui:** add toggle panel button ([7674d7d](https://github.com/jtheoof/swappy/commit/7674d7db8ba8d97302a045af8d2383de37acb2d1)), closes [#24](https://github.com/jtheoof/swappy/issues/24) 149 | * **ui:** add undo/redo ([bcc1314](https://github.com/jtheoof/swappy/commit/bcc13140ebfdefa30431b288f089d23bb1df743e)) 150 | * **ui:** life is full of colors and joy ([606cd34](https://github.com/jtheoof/swappy/commit/606cd3459de3908e5fecdb7a49162ef3a9b52ab7)) 151 | * **ui:** replace popover by on screen elements ([8cd3f13](https://github.com/jtheoof/swappy/commit/8cd3f134bbd8e05523303914f6c8f3989e6b4502)) 152 | * **wayland:** added xdg_output_manager ([7b3549f](https://github.com/jtheoof/swappy/commit/7b3549fdd86fe1a945e1988bf22042c0f8dd6ed0)) 153 | * **wayland:** listing outputs ([5a55c8b](https://github.com/jtheoof/swappy/commit/5a55c8bbbd08ad717ddabac51be31483950d827f)) 154 | 155 | 156 | ### Bug Fixes 157 | 158 | * **application:** fix file loop and use of GTK object after lifecycle ([320dae0](https://github.com/jtheoof/swappy/commit/320dae02d0c6dca3fa2fd7ca934a85483ac2dd35)) 159 | * **application:** memory leak for pixbuf ([f9d70fc](https://github.com/jtheoof/swappy/commit/f9d70fc0e22274e6cbe74bfdf714cdf04e34053d)) 160 | * **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) 161 | * **application:** suffix saved file with png ([7f2f6da](https://github.com/jtheoof/swappy/commit/7f2f6da754571771475558233f5a47813ec278dd)) 162 | * **blur:** adjust blur bounding box based on scaled monitor ([6b2ec90](https://github.com/jtheoof/swappy/commit/6b2ec90efd99e1979310b673ad40b3724669dac1)) 163 | * **blur:** blur based on device scaling factor ([1699474](https://github.com/jtheoof/swappy/commit/1699474c39fc305492c8bb03063c4582af4dbf9e)) 164 | * **blur:** use better glyph icon ([97cd607](https://github.com/jtheoof/swappy/commit/97cd6072c986c9a7c69306744390a6ddb6a44646)) 165 | * **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) 166 | * **buffer:** properly include required functions ([d787586](https://github.com/jtheoof/swappy/commit/d787586b9ed1d7e855ae2d416914d619636f41b1)), closes [#10](https://github.com/jtheoof/swappy/issues/10) 167 | * **clipboard:** handle bad write to pipe fd ([f963a76](https://github.com/jtheoof/swappy/commit/f963a76c5c01b9b5f81b97118bf1b9e6990d995d)) 168 | * **clipboard:** memory leak for pixbuf ([665295b](https://github.com/jtheoof/swappy/commit/665295b497d7ef124d5a2eeb7eb76964fdb3566a)) 169 | * **dependencies:** include glib2 ([992d97e](https://github.com/jtheoof/swappy/commit/992d97e94d2ebd32ac3e1901910050fae1954ed0)), closes [#11](https://github.com/jtheoof/swappy/issues/11) 170 | * **file:** properly check file system errors if any ([541ec21](https://github.com/jtheoof/swappy/commit/541ec21ca0efdec4d06c96f5ad1768b4219ed4ab)) 171 | * **init:** fix segfault for unknown flags ([f4e9a19](https://github.com/jtheoof/swappy/commit/f4e9a19407d8d1bfa59c08f6bf97617c662e1ac0)) 172 | * **init:** properly handle null geometry ([c4ea305](https://github.com/jtheoof/swappy/commit/c4ea305ae6ac9429bf44fdfc7218a30363439582)) 173 | * **man:** remove blur_level related config ([ceb907a](https://github.com/jtheoof/swappy/commit/ceb907a5dc736c7d44318b35fb911aeb2360d851)) 174 | * **meson:** able to build on standard platforms ([8abc5d5](https://github.com/jtheoof/swappy/commit/8abc5d52ec2962a111c6d44cdb5e9e209ac219c7)) 175 | * **meson:** remove useless cname in meson res file ([9b8ea64](https://github.com/jtheoof/swappy/commit/9b8ea64307b33eb010b8ba043919f3eddf935b19)) 176 | * **paint:** fix memory leak for brush paints ([aed2bfe](https://github.com/jtheoof/swappy/commit/aed2bfe29465aa5161155c1edda9d03cac607906)) 177 | * **pixbuf:** possibly fix core dump ([8a82e79](https://github.com/jtheoof/swappy/commit/8a82e796bb871b57fa6ab4d2ed8d761033370d8c)) 178 | * **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) 179 | * **render:** better handler empty buffer ([acf2379](https://github.com/jtheoof/swappy/commit/acf2379ba3117ba6eb8c426e85a60ce71a3abe67)) 180 | * **render:** draw from last to first ([4b69ada](https://github.com/jtheoof/swappy/commit/4b69ada9a1469d3b6e106e07bf7155836b31d613)) 181 | * **render:** fix arrow glitch with 0 ftx ([ec6e6ab](https://github.com/jtheoof/swappy/commit/ec6e6abae7629800fec4c715957c4932946f51ed)) 182 | * **render:** properly scale arrow along with stroke size ([75bfc10](https://github.com/jtheoof/swappy/commit/75bfc10fb7a5507b66bd6d19ab06f2f6a393bb6a)) 183 | * **resources:** compile resources and fix error management ([05d87c9](https://github.com/jtheoof/swappy/commit/05d87c929ff8b3311cd5db111cd2f53a32c35a19)) 184 | * **string:** fix algo to insert chars at location ([bc3264e](https://github.com/jtheoof/swappy/commit/bc3264e9f11bb4f3a02d7f5ae92ef8a4d2b42513)) 185 | * **ui:** add stroke size increase/decrease/reset ([5930c99](https://github.com/jtheoof/swappy/commit/5930c99b9e0208148d6bc8cf0fc3aa8f69dbd36d)) 186 | * **ui:** move paint area inside GtkFixed ([50e7c97](https://github.com/jtheoof/swappy/commit/50e7c97042805f5550d2a62d45c8e49208d7632d)) 187 | * **ui:** prevent focus in panel buttons ([903ad11](https://github.com/jtheoof/swappy/commit/903ad114f516981c8d0644f704af9c722f74a61f)) 188 | * **ui:** small tweaks ([2b73777](https://github.com/jtheoof/swappy/commit/2b73777142141598c14d37d1b6fa9573de12d914)) 189 | * **ui:** tweak button sizes ([425f455](https://github.com/jtheoof/swappy/commit/425f455ab7665a046060fe140c861aeb7ea8209b)) 190 | * **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) 191 | * **wayland:** initialize done copies to 0 ([65cefc1](https://github.com/jtheoof/swappy/commit/65cefc1da7fed86508301250ffc1b6dbc9fd3692)) 192 | * **wayland:** replace g_error by g_warning ([64bfc2b](https://github.com/jtheoof/swappy/commit/64bfc2b3a71ed00d0dc1102501ac85792735833f)) 193 | * **window:** quit when delete event is received ([0c5e458](https://github.com/jtheoof/swappy/commit/0c5e458d4c44a2e2e2b4451b4576724aef2a06b0)) 194 | 195 | 196 | * refactor!(wayland): remove wayland code ([204a93e](https://github.com/jtheoof/swappy/commit/204a93eb0f696bc7be8335d46212c6024e3b2c51)) 197 | -------------------------------------------------------------------------------- /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 | ``` 57 | 58 | - `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 59 | - `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 60 | - `show_panel` is used to toggle the paint panel on or off upon startup 61 | - `line_size` is the default line size (must be between 1 and 50) 62 | - `text_size` is the default text size (must be between 10 and 50) 63 | - `text_font` is the font used to render text, its format is pango friendly 64 | - `paint_mode` is the mode activated at application start (must be one of: brush|text|rectangle|ellipse|arrow|blur, matching is case-insensitive) 65 | - `early_exit` is used to make the application exit after saving the picture or copying it to the clipboard 66 | - `fill_shape` is used to toggle shape filling (for the rectangle and ellipsis tools) on or off upon startup 67 | - `auto_save` is used to toggle auto saving of final buffer to `save_dir` upon exit 68 | - `custom_color` is used to set a default value for the custom color 69 | 70 | 71 | ## Keyboard Shortcuts 72 | 73 | - `Ctrl+b`: Toggle Paint Panel 74 | 75 |
76 | 77 | - `b`: Switch to Brush 78 | - `t`: Switch to Text 79 | - `r`: Switch to Rectangle 80 | - `o`: Switch to Ellipse 81 | - `a`: Switch to Arrow 82 | - `d`: Switch to Blur (`d` stands for droplet) 83 | 84 |
85 | 86 | - `R`: Use Red Color 87 | - `G`: Use Green Color 88 | - `B`: Use Blue Color 89 | - `C`: Use Custom Color 90 | - `Minus`: Reduce Stroke Size 91 | - `Plus`: Increase Stroke Size 92 | - `Equal`: Reset Stroke Size 93 | - `f`: Toggle Shape Filling 94 | - `k`: Clear Paints (cannot be undone) 95 | 96 |
97 | 98 | - `Ctrl`: Center Shape (Rectangle & Ellipse) based on draw start 99 | 100 |
101 | 102 | - `Ctrl+z`: Undo 103 | - `Ctrl+Shift+z` or `Ctrl+y`: Redo 104 | - `Ctrl+s`: Save to file (see man page) 105 | - `Ctrl+c`: Copy to clipboard 106 | - `Escape` or `q` or `Ctrl+w`: Quit swappy 107 | 108 | ## Limitations 109 | 110 | - **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. 111 | - **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` 112 | 113 | ## Installation 114 | 115 | - [Arch Linux](https://archlinux.org/packages/extra/x86_64/swappy/) 116 | - [Arch Linux (git)](https://aur.archlinux.org/packages/swappy-git) 117 | - [Fedora](https://src.fedoraproject.org/rpms/swappy) 118 | - [Gentoo](https://packages.gentoo.org/packages/gui-apps/swappy) 119 | - [openSUSE](https://build.opensuse.org/package/show/X11:Wayland/swappy) 120 | - [Void Linux](https://github.com/void-linux/void-packages/tree/master/srcpkgs/swappy) 121 | 122 | ## Building from source 123 | 124 | Install dependencies (on Arch, name can vary for other distros): 125 | 126 | - meson 127 | - ninja 128 | - cairo 129 | - pango 130 | - gtk 131 | - glib2 132 | - scdoc 133 | 134 | Optional dependencies: 135 | 136 | - `wl-clipboard` (to make sure the copy is saved if you close swappy) 137 | - `otf-font-awesome` (to draw the paint icons properly) 138 | 139 | Then run: 140 | 141 | ```sh 142 | meson setup build 143 | ninja -C build 144 | ``` 145 | 146 | ### i18n 147 | 148 | This section is for developers, maintainers and translators. 149 | 150 | To add support to a new locale or when translations are updated: 151 | 152 | 1. Update `src/po/LINGUAS` (when new locales are added) 153 | 2. Generate a new `po` file (ignore and do not commit potential noise in other files): 154 | 155 | ```sh 156 | ninja -C build swappy-update-po 157 | ``` 158 | 159 | To rebuild the base template (should happen less often): 160 | 161 | ```sh 162 | ninja -C build swappy-pot 163 | ``` 164 | 165 | See the [meson documentation](https://mesonbuild.com/Localisation.html) for details. 166 | 167 | ## Contributing 168 | 169 | Pull requests are welcome. This project uses [conventional commits](https://www.conventionalcommits.org/en/v1.0.0/) to automate changelog generation. 170 | 171 | ## Release 172 | 173 | 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. 174 | 175 | ```sh 176 | ./script/github-release 177 | ``` 178 | 179 | Make sure everything is valid in the Draft release, then publish the draft. 180 | 181 | Release tarballs are signed with this PGP key: `F44D05A50F6C9EB5C81BCF966A6B35DBE9442683` 182 | 183 | ## License 184 | 185 | MIT 186 | 187 | [snappy]: http://snappy-app.com/ 188 | [slurp]: https://github.com/emersion/slurp 189 | [grim]: https://github.com/emersion/grim 190 | [sway]: https://github.com/swaywm/sway 191 | [wl-clipboard]: https://github.com/bugaevc/wl-clipboard 192 | -------------------------------------------------------------------------------- /docs/images/screenshot-1.0.0.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/2aa3ae2433ee671ddc73e36ece8598e68f7f3632/docs/images/screenshot-1.0.0.png -------------------------------------------------------------------------------- /docs/images/screenshot-beta.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/2aa3ae2433ee671ddc73e36ece8598e68f7f3632/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 | -------------------------------------------------------------------------------- /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_SHOW_PANEL_DEFAULT false 7 | #define CONFIG_SAVE_FILENAME_FORMAT_DEFAULT "swappy-%Y%m%d_%H%M%S.png" 8 | #define CONFIG_PAINT_MODE_DEFAULT SWAPPY_PAINT_MODE_BRUSH 9 | #define CONFIG_EARLY_EXIT_DEFAULT false 10 | #define CONFIG_FILL_SHAPE_DEFAULT false 11 | #define CONFIG_AUTO_SAVE_DEFAULT false 12 | #define CONFIG_CUSTOM_COLOR_DEFAULT "rgba(193,125,17,1)" 13 | 14 | void config_load(struct swappy_state *state); 15 | void config_free(struct swappy_state *state); 16 | -------------------------------------------------------------------------------- /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_text_clip(struct swappy_state *state, gdouble x, 14 | gdouble y); 15 | void paint_commit_temporary(struct swappy_state *state); 16 | 17 | void paint_free(gpointer data); 18 | void paint_free_all(struct swappy_state *state); 19 | void paint_free_list(GList **list); 20 | -------------------------------------------------------------------------------- /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 | enum swappy_paint_type { 18 | SWAPPY_PAINT_MODE_BRUSH = 0, /* Brush mode to draw arbitrary shapes */ 19 | SWAPPY_PAINT_MODE_TEXT, /* Mode to draw texts */ 20 | SWAPPY_PAINT_MODE_RECTANGLE, /* Rectangle shapes */ 21 | SWAPPY_PAINT_MODE_ELLIPSE, /* Ellipse shapes */ 22 | SWAPPY_PAINT_MODE_ARROW, /* Arrow shapes */ 23 | SWAPPY_PAINT_MODE_BLUR, /* Blur mode */ 24 | }; 25 | 26 | enum swappy_paint_shape_operation { 27 | SWAPPY_PAINT_SHAPE_OPERATION_STROKE = 0, /* Used to stroke the shape */ 28 | SWAPPY_PAINT_SHAPE_OPERATION_FILL, /* Used to fill the shape */ 29 | }; 30 | 31 | enum swappy_text_mode { 32 | SWAPPY_TEXT_MODE_EDIT = 0, 33 | SWAPPY_TEXT_MODE_DONE, 34 | }; 35 | 36 | struct swappy_point { 37 | gdouble x; 38 | gdouble y; 39 | }; 40 | 41 | struct swappy_paint_text { 42 | double r; 43 | double g; 44 | double b; 45 | double a; 46 | double s; 47 | gchar *font; 48 | gchar *text; 49 | glong cursor; 50 | struct swappy_point from; 51 | struct swappy_point to; 52 | enum swappy_text_mode mode; 53 | }; 54 | 55 | struct swappy_paint_shape { 56 | double r; 57 | double g; 58 | double b; 59 | double a; 60 | double w; 61 | bool should_center_at_from; 62 | struct swappy_point from; 63 | struct swappy_point to; 64 | enum swappy_paint_type type; 65 | enum swappy_paint_shape_operation operation; 66 | }; 67 | 68 | struct swappy_paint_brush { 69 | double r; 70 | double g; 71 | double b; 72 | double a; 73 | double w; 74 | GList *points; 75 | }; 76 | 77 | struct swappy_paint_blur { 78 | struct swappy_point from; 79 | struct swappy_point to; 80 | cairo_surface_t *surface; 81 | }; 82 | 83 | struct swappy_paint { 84 | enum swappy_paint_type type; 85 | bool can_draw; 86 | bool is_committed; 87 | union { 88 | struct swappy_paint_brush brush; 89 | struct swappy_paint_shape shape; 90 | struct swappy_paint_text text; 91 | struct swappy_paint_blur blur; 92 | } content; 93 | }; 94 | 95 | struct swappy_box { 96 | int32_t x; 97 | int32_t y; 98 | int32_t width; 99 | int32_t height; 100 | }; 101 | 102 | struct swappy_state_settings { 103 | double r; 104 | double g; 105 | double b; 106 | double a; 107 | double w; 108 | double t; 109 | }; 110 | 111 | struct swappy_state_ui { 112 | gboolean panel_toggled; 113 | 114 | GtkWindow *window; 115 | GtkWidget *area; 116 | 117 | GtkToggleButton *panel_toggle_button; 118 | 119 | // Undo / Redo 120 | GtkButton *undo; 121 | GtkButton *redo; 122 | 123 | // Painting Area 124 | GtkBox *painting_box; 125 | GtkRadioButton *brush; 126 | GtkRadioButton *text; 127 | GtkRadioButton *rectangle; 128 | GtkRadioButton *ellipse; 129 | GtkRadioButton *arrow; 130 | GtkRadioButton *blur; 131 | 132 | GtkRadioButton *red; 133 | GtkRadioButton *green; 134 | GtkRadioButton *blue; 135 | GtkRadioButton *custom; 136 | GtkColorButton *color; 137 | 138 | GtkButton *line_size; 139 | GtkButton *text_size; 140 | 141 | GtkToggleButton *fill_shape; 142 | }; 143 | 144 | struct swappy_config { 145 | char *config_file; 146 | char *save_dir; 147 | char *save_filename_format; 148 | gint8 paint_mode; 149 | gboolean fill_shape; 150 | gboolean show_panel; 151 | guint32 line_size; 152 | guint32 text_size; 153 | char *text_font; 154 | gboolean early_exit; 155 | gboolean auto_save; 156 | char *custom_color; 157 | }; 158 | 159 | struct swappy_state { 160 | GtkApplication *app; 161 | 162 | struct swappy_state_ui *ui; 163 | struct swappy_config *config; 164 | 165 | GdkPixbuf *original_image; 166 | cairo_surface_t *original_image_surface; 167 | cairo_surface_t *rendering_surface; 168 | 169 | gdouble scaling_factor; 170 | 171 | enum swappy_paint_type mode; 172 | 173 | /* Options */ 174 | char *file_str; 175 | char *output_file; 176 | 177 | char *temp_file_str; 178 | 179 | struct swappy_box *window; 180 | struct swappy_box *geometry; 181 | 182 | GList *paints; 183 | GList *redo_paints; 184 | struct swappy_paint *temp_paint; 185 | 186 | struct swappy_state_settings settings; 187 | 188 | int argc; 189 | char **argv; 190 | }; 191 | -------------------------------------------------------------------------------- /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.5.1', 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.5.1", 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.glade: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | True 7 | False 8 | edit-redo-symbolic 9 | 10 | 11 | True 12 | False 13 | edit-undo-symbolic 14 | 15 | 16 | True 17 | False 18 | edit-delete-symbolic 19 | 20 | 21 | True 22 | False 23 | edit-copy-symbolic 24 | 25 | 26 | True 27 | False 28 | document-save-symbolic 29 | 30 | 31 | True 32 | False 33 | document-properties-symbolic 34 | 35 | 36 | True 37 | False 38 | zoom-in-symbolic 39 | 40 | 41 | True 42 | False 43 | zoom-in-symbolic 44 | 45 | 46 | True 47 | False 48 | zoom-in-symbolic 49 | 50 | 51 | True 52 | False 53 | zoom-out-symbolic 54 | 55 | 56 | True 57 | False 58 | zoom-out-symbolic 59 | 60 | 61 | True 62 | False 63 | False 64 | center 65 | False 66 | 67 | 68 | 69 | 70 | 71 | True 72 | False 73 | 74 | 75 | 100 76 | 80 77 | True 78 | True 79 | 80 | 81 | False 82 | 10 83 | 10 84 | 10 85 | 10 86 | vertical 87 | 88 | 89 | True 90 | False 91 | True 92 | 93 | 94 | True 95 | False 96 | B 97 | 98 | 99 | False 100 | True 101 | 0 102 | 103 | 104 | 105 | 106 | True 107 | False 108 | T 109 | 110 | 111 | False 112 | True 113 | 1 114 | 115 | 116 | 117 | 118 | True 119 | False 120 | R 121 | 122 | 123 | False 124 | True 125 | 2 126 | 127 | 128 | 129 | 130 | True 131 | False 132 | O 133 | 134 | 135 | False 136 | True 137 | 3 138 | 139 | 140 | 141 | 142 | True 143 | False 144 | A 145 | 146 | 147 | False 148 | True 149 | 4 150 | 151 | 152 | 153 | 154 | True 155 | False 156 | D 157 | 158 | 159 | False 160 | True 161 | 5 162 | 163 | 164 | 165 | 166 | False 167 | True 168 | 0 169 | 170 | 171 | 172 | 173 | True 174 | False 175 | 15 176 | 6 177 | True 178 | 179 | 180 | 181 | True 182 | False 183 | False 184 | True 185 | False 186 | 187 | 188 | 189 | False 190 | True 191 | 0 192 | 193 | 194 | 195 | 196 | 197 | True 198 | False 199 | False 200 | False 201 | brush 202 | 203 | 204 | 205 | False 206 | True 207 | 1 208 | 209 | 210 | 211 | 212 | 213 | True 214 | False 215 | False 216 | False 217 | brush 218 | 219 | 220 | 221 | False 222 | True 223 | 2 224 | 225 | 226 | 227 | 228 | 229 | True 230 | False 231 | False 232 | False 233 | brush 234 | 235 | 236 | 237 | False 238 | True 239 | 3 240 | 241 | 242 | 243 | 244 | 245 | True 246 | False 247 | False 248 | False 249 | brush 250 | 251 | 252 | 253 | False 254 | True 255 | 4 256 | 257 | 258 | 259 | 260 | 261 | True 262 | False 263 | False 264 | False 265 | brush 266 | 267 | 268 | 269 | False 270 | True 271 | 5 272 | 273 | 274 | 277 | 278 | 279 | False 280 | True 281 | 1 282 | 283 | 284 | 285 | 286 | True 287 | False 288 | center 289 | 15 290 | 291 | 292 | True 293 | False 294 | start 295 | baseline 296 | 10 297 | 298 | 299 | True 300 | False 301 | False 302 | True 303 | center 304 | False 305 | 306 | 307 | 308 | True 309 | False 310 | 311 | 312 | 315 | 316 | 317 | False 318 | True 319 | 0 320 | 321 | 322 | 323 | 324 | True 325 | False 326 | False 327 | True 328 | center 329 | False 330 | color-red-button 331 | 332 | 333 | 334 | True 335 | False 336 | 337 | 338 | 341 | 342 | 343 | False 344 | True 345 | 1 346 | 347 | 348 | 349 | 350 | True 351 | False 352 | False 353 | True 354 | center 355 | False 356 | color-red-button 357 | 358 | 359 | 360 | True 361 | False 362 | 363 | 364 | 367 | 368 | 369 | False 370 | True 371 | 2 372 | 373 | 374 | 377 | 378 | 379 | False 380 | True 381 | 0 382 | 383 | 384 | 385 | 386 | True 387 | False 388 | 5 389 | 390 | 391 | True 392 | False 393 | False 394 | True 395 | center 396 | False 397 | color-red-button 398 | 399 | 400 | 401 | True 402 | False 403 | gtk-color-picker 404 | 405 | 406 | 407 | 408 | False 409 | True 410 | 0 411 | 412 | 413 | 414 | 415 | True 416 | False 417 | False 418 | True 419 | center 420 | 421 | rgb(193,125,17) 422 | 423 | 424 | 425 | False 426 | True 427 | 1 428 | 429 | 430 | 431 | 432 | False 433 | True 434 | 25 435 | 1 436 | 437 | 438 | 439 | 440 | False 441 | False 442 | 2 443 | 444 | 445 | 446 | 447 | True 448 | False 449 | 10 450 | 2 451 | True 452 | 453 | 454 | True 455 | False 456 | Line Width 457 | 458 | 459 | False 460 | True 461 | 0 462 | 463 | 464 | 465 | 466 | True 467 | False 468 | True 469 | zoom-out 470 | True 471 | 472 | 473 | 474 | False 475 | False 476 | 1 477 | 478 | 479 | 480 | 481 | True 482 | False 483 | True 484 | True 485 | 486 | 487 | 488 | False 489 | True 490 | 2 491 | 492 | 493 | 494 | 495 | True 496 | False 497 | True 498 | zoom-in 499 | True 500 | 501 | 502 | 503 | False 504 | False 505 | 3 506 | 507 | 508 | 509 | 510 | False 511 | True 512 | 3 513 | 514 | 515 | 516 | 517 | True 518 | False 519 | 10 520 | 2 521 | True 522 | 523 | 524 | True 525 | False 526 | Text Size 527 | 528 | 529 | False 530 | True 531 | 0 532 | 533 | 534 | 535 | 536 | True 537 | True 538 | True 539 | zoom-out1 540 | True 541 | 542 | 543 | 544 | False 545 | False 546 | 1 547 | 548 | 549 | 550 | 551 | True 552 | True 553 | True 554 | True 555 | 556 | 557 | 558 | False 559 | True 560 | 2 561 | 562 | 563 | 564 | 565 | True 566 | True 567 | True 568 | zoom-in1 569 | True 570 | 571 | 572 | 573 | False 574 | False 575 | 3 576 | 577 | 578 | 579 | 580 | False 581 | True 582 | 4 583 | 584 | 585 | 586 | 587 | True 588 | False 589 | True 590 | 591 | 592 | Fill shape 593 | True 594 | False 595 | False 596 | True 597 | Toggle shape filling 598 | True 599 | 600 | 601 | 602 | False 603 | True 604 | 1 605 | 606 | 607 | 608 | 609 | False 610 | True 611 | 5 612 | 613 | 614 | 615 | 616 | False 617 | False 618 | 619 | 620 | 621 | 622 | True 623 | False 624 | center 625 | center 626 | 627 | 628 | True 629 | False 630 | GDK_POINTER_MOTION_MASK | GDK_BUTTON1_MOTION_MASK | GDK_BUTTON_PRESS_MASK | GDK_BUTTON_RELEASE_MASK 631 | 10 632 | 10 633 | 10 634 | 10 635 | 636 | 637 | 638 | 639 | 640 | 641 | 642 | 643 | 644 | True 645 | True 646 | 647 | 648 | 649 | 650 | -1 651 | 652 | 653 | 654 | 655 | 656 | 657 | True 658 | False 659 | 10 660 | 661 | 662 | True 663 | False 664 | vertical 665 | 666 | 667 | True 668 | False 669 | False 670 | True 671 | Toggle Paint Panel 672 | img-toggle-panel 673 | True 674 | 675 | 676 | 677 | False 678 | True 679 | 0 680 | 681 | 682 | 683 | 684 | 685 | 686 | True 687 | False 688 | 5 689 | start 690 | 691 | 692 | True 693 | False 694 | False 695 | False 696 | True 697 | Undo Last Paint 698 | edit-undo 699 | True 700 | 701 | 702 | 703 | True 704 | True 705 | 0 706 | True 707 | 708 | 709 | 710 | 711 | True 712 | False 713 | False 714 | False 715 | True 716 | Redo Previous Paint 717 | edit-redo 718 | True 719 | 720 | 721 | 722 | 723 | 724 | True 725 | True 726 | 1 727 | True 728 | 729 | 730 | 731 | 732 | True 733 | False 734 | True 735 | Clear Paints 736 | img-clear-paints 737 | True 738 | 739 | 740 | 741 | True 742 | True 743 | 2 744 | True 745 | True 746 | 747 | 748 | 749 | 750 | 1 751 | 752 | 753 | 754 | 755 | True 756 | False 757 | 5 758 | 759 | 760 | True 761 | False 762 | True 763 | Copy Surface 764 | img-copy-surface 765 | True 766 | 767 | 768 | 769 | False 770 | True 771 | 1 772 | 773 | 774 | 775 | 776 | True 777 | False 778 | True 779 | Save Surface 780 | img-save-surface 781 | True 782 | 783 | 784 | 785 | False 786 | True 787 | 2 788 | 789 | 790 | 791 | 792 | end 793 | 2 794 | 795 | 796 | 797 | 798 | True 799 | False 800 | 5 801 | 802 | 803 | True 804 | False 805 | 806 | 807 | False 808 | True 809 | 0 810 | 811 | 812 | 813 | 814 | 3 815 | 816 | 817 | 818 | 819 | 820 | 821 | -------------------------------------------------------------------------------- /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 | 8 | #include "clipboard.h" 9 | #include "config.h" 10 | #include "file.h" 11 | #include "paint.h" 12 | #include "pixbuf.h" 13 | #include "render.h" 14 | #include "swappy.h" 15 | 16 | static void update_ui_undo_redo(struct swappy_state *state) { 17 | GtkWidget *undo = GTK_WIDGET(state->ui->undo); 18 | GtkWidget *redo = GTK_WIDGET(state->ui->redo); 19 | gboolean undo_sensitive = g_list_length(state->paints) > 0; 20 | gboolean redo_sensitive = g_list_length(state->redo_paints) > 0; 21 | gtk_widget_set_sensitive(undo, undo_sensitive); 22 | gtk_widget_set_sensitive(redo, redo_sensitive); 23 | } 24 | 25 | static void update_ui_stroke_size_widget(struct swappy_state *state) { 26 | GtkButton *button = GTK_BUTTON(state->ui->line_size); 27 | char label[255]; 28 | g_snprintf(label, 255, "%.0lf", state->settings.w); 29 | gtk_button_set_label(button, label); 30 | } 31 | 32 | static void update_ui_text_size_widget(struct swappy_state *state) { 33 | GtkButton *button = GTK_BUTTON(state->ui->text_size); 34 | char label[255]; 35 | g_snprintf(label, 255, "%.0lf", state->settings.t); 36 | gtk_button_set_label(button, label); 37 | } 38 | 39 | static void update_ui_panel_toggle_button(struct swappy_state *state) { 40 | GtkWidget *painting_box = GTK_WIDGET(state->ui->painting_box); 41 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(state->ui->panel_toggle_button); 42 | gboolean toggled = state->ui->panel_toggled; 43 | 44 | gtk_toggle_button_set_active(button, toggled); 45 | gtk_widget_set_visible(painting_box, toggled); 46 | } 47 | 48 | static void update_ui_fill_shape_toggle_button(struct swappy_state *state) { 49 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(state->ui->fill_shape); 50 | gboolean toggled = state->config->fill_shape; 51 | 52 | gtk_toggle_button_set_active(button, toggled); 53 | } 54 | 55 | void application_finish(struct swappy_state *state) { 56 | g_debug("application finishing, cleaning up"); 57 | paint_free_all(state); 58 | pixbuf_free(state); 59 | cairo_surface_destroy(state->rendering_surface); 60 | cairo_surface_destroy(state->original_image_surface); 61 | if (state->temp_file_str) { 62 | g_info("deleting temporary file: %s", state->temp_file_str); 63 | if (g_unlink(state->temp_file_str) != 0) { 64 | g_warning("unable to delete temporary file: %s", state->temp_file_str); 65 | } 66 | g_free(state->temp_file_str); 67 | } 68 | g_free(state->file_str); 69 | g_free(state->geometry); 70 | g_free(state->window); 71 | g_free(state->ui); 72 | 73 | g_object_unref(state->app); 74 | 75 | config_free(state); 76 | } 77 | 78 | static void action_undo(struct swappy_state *state) { 79 | GList *first = state->paints; 80 | 81 | if (first) { 82 | state->paints = g_list_remove_link(state->paints, first); 83 | state->redo_paints = g_list_prepend(state->redo_paints, first->data); 84 | 85 | render_state(state); 86 | update_ui_undo_redo(state); 87 | } 88 | } 89 | 90 | static void action_redo(struct swappy_state *state) { 91 | GList *first = state->redo_paints; 92 | 93 | if (first) { 94 | state->redo_paints = g_list_remove_link(state->redo_paints, first); 95 | state->paints = g_list_prepend(state->paints, first->data); 96 | 97 | render_state(state); 98 | update_ui_undo_redo(state); 99 | } 100 | } 101 | 102 | static void action_clear(struct swappy_state *state) { 103 | paint_free_all(state); 104 | render_state(state); 105 | update_ui_undo_redo(state); 106 | } 107 | 108 | static void action_toggle_painting_panel(struct swappy_state *state, 109 | gboolean *toggled) { 110 | state->ui->panel_toggled = 111 | (toggled == NULL) ? !state->ui->panel_toggled : *toggled; 112 | update_ui_panel_toggle_button(state); 113 | } 114 | 115 | static void action_update_color_state(struct swappy_state *state, double r, 116 | double g, double b, double a, 117 | gboolean custom) { 118 | state->settings.r = r; 119 | state->settings.g = g; 120 | state->settings.b = b; 121 | state->settings.a = a; 122 | 123 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->color), custom); 124 | } 125 | 126 | static void action_set_color_from_custom(struct swappy_state *state) { 127 | GdkRGBA color; 128 | gtk_color_chooser_get_rgba(GTK_COLOR_CHOOSER(state->ui->color), &color); 129 | 130 | action_update_color_state(state, color.red, color.green, color.blue, 131 | color.alpha, true); 132 | } 133 | 134 | static void switch_mode_to_brush(struct swappy_state *state) { 135 | state->mode = SWAPPY_PAINT_MODE_BRUSH; 136 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 137 | } 138 | 139 | static void switch_mode_to_text(struct swappy_state *state) { 140 | state->mode = SWAPPY_PAINT_MODE_TEXT; 141 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 142 | } 143 | 144 | static void switch_mode_to_rectangle(struct swappy_state *state) { 145 | state->mode = SWAPPY_PAINT_MODE_RECTANGLE; 146 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true); 147 | } 148 | 149 | static void switch_mode_to_ellipse(struct swappy_state *state) { 150 | state->mode = SWAPPY_PAINT_MODE_ELLIPSE; 151 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true); 152 | } 153 | 154 | static void switch_mode_to_arrow(struct swappy_state *state) { 155 | state->mode = SWAPPY_PAINT_MODE_ARROW; 156 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 157 | } 158 | 159 | static void switch_mode_to_blur(struct swappy_state *state) { 160 | state->mode = SWAPPY_PAINT_MODE_BLUR; 161 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 162 | } 163 | 164 | static void action_stroke_size_decrease(struct swappy_state *state) { 165 | guint step = state->settings.w <= 10 ? 1 : 5; 166 | 167 | state->settings.w -= step; 168 | 169 | if (state->settings.w < SWAPPY_LINE_SIZE_MIN) { 170 | state->settings.w = SWAPPY_LINE_SIZE_MIN; 171 | } 172 | 173 | update_ui_stroke_size_widget(state); 174 | } 175 | 176 | static void action_stroke_size_reset(struct swappy_state *state) { 177 | state->settings.w = state->config->line_size; 178 | 179 | update_ui_stroke_size_widget(state); 180 | } 181 | 182 | static void action_stroke_size_increase(struct swappy_state *state) { 183 | guint step = state->settings.w >= 10 ? 5 : 1; 184 | state->settings.w += step; 185 | 186 | if (state->settings.w > SWAPPY_LINE_SIZE_MAX) { 187 | state->settings.w = SWAPPY_LINE_SIZE_MAX; 188 | } 189 | 190 | update_ui_stroke_size_widget(state); 191 | } 192 | 193 | static void action_text_size_decrease(struct swappy_state *state) { 194 | guint step = state->settings.t <= 20 ? 1 : 5; 195 | state->settings.t -= step; 196 | 197 | if (state->settings.t < SWAPPY_TEXT_SIZE_MIN) { 198 | state->settings.t = SWAPPY_TEXT_SIZE_MIN; 199 | } 200 | 201 | update_ui_text_size_widget(state); 202 | } 203 | static void action_text_size_reset(struct swappy_state *state) { 204 | state->settings.t = state->config->text_size; 205 | update_ui_text_size_widget(state); 206 | } 207 | static void action_text_size_increase(struct swappy_state *state) { 208 | guint step = state->settings.t >= 20 ? 5 : 1; 209 | state->settings.t += step; 210 | 211 | if (state->settings.t > SWAPPY_TEXT_SIZE_MAX) { 212 | state->settings.t = SWAPPY_TEXT_SIZE_MAX; 213 | } 214 | 215 | update_ui_text_size_widget(state); 216 | } 217 | 218 | static void action_fill_shape_toggle(struct swappy_state *state, 219 | gboolean *toggled) { 220 | // Don't allow changing the state via a shortcut if the button can't be 221 | // clicked. 222 | if (!gtk_widget_get_sensitive(GTK_WIDGET(state->ui->fill_shape))) return; 223 | 224 | gboolean toggle = (toggled == NULL) ? !state->config->fill_shape : *toggled; 225 | state->config->fill_shape = toggle; 226 | 227 | update_ui_fill_shape_toggle_button(state); 228 | } 229 | 230 | static void save_state_to_file_or_folder(struct swappy_state *state, 231 | char *file) { 232 | GdkPixbuf *pixbuf = pixbuf_get_from_state(state); 233 | 234 | if (file == NULL) { 235 | pixbuf_save_state_to_folder(pixbuf, state->config->save_dir, 236 | state->config->save_filename_format); 237 | } else { 238 | pixbuf_save_to_file(pixbuf, file); 239 | } 240 | 241 | g_object_unref(pixbuf); 242 | 243 | if (state->config->early_exit) { 244 | gtk_main_quit(); 245 | } 246 | } 247 | 248 | static void maybe_save_output_file(struct swappy_state *state) { 249 | if (state->config->auto_save) { 250 | save_state_to_file_or_folder(state, state->output_file); 251 | } 252 | } 253 | 254 | static void screen_coordinates_to_image_coordinates(struct swappy_state *state, 255 | gdouble screen_x, 256 | gdouble screen_y, 257 | gdouble *image_x, 258 | gdouble *image_y) { 259 | gdouble x, y; 260 | 261 | gint w = gdk_pixbuf_get_width(state->original_image); 262 | gint h = gdk_pixbuf_get_height(state->original_image); 263 | 264 | // Clamp coordinates to original image properties to avoid side effects in 265 | // rendering pipeline 266 | x = CLAMP(screen_x / state->scaling_factor, 0, w); 267 | y = CLAMP(screen_y / state->scaling_factor, 0, h); 268 | 269 | *image_x = x; 270 | *image_y = y; 271 | } 272 | 273 | static void commit_state(struct swappy_state *state) { 274 | paint_commit_temporary(state); 275 | paint_free_list(&state->redo_paints); 276 | render_state(state); 277 | update_ui_undo_redo(state); 278 | } 279 | 280 | void on_destroy(GtkApplication *application, gpointer data) { 281 | struct swappy_state *state = (struct swappy_state *)data; 282 | maybe_save_output_file(state); 283 | } 284 | 285 | void brush_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 286 | switch_mode_to_brush(state); 287 | } 288 | 289 | void text_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 290 | switch_mode_to_text(state); 291 | } 292 | 293 | void rectangle_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 294 | switch_mode_to_rectangle(state); 295 | } 296 | 297 | void ellipse_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 298 | switch_mode_to_ellipse(state); 299 | } 300 | 301 | void arrow_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 302 | switch_mode_to_arrow(state); 303 | } 304 | 305 | void blur_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 306 | switch_mode_to_blur(state); 307 | } 308 | 309 | void save_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 310 | // Commit a potential paint (e.g. text being written) 311 | commit_state(state); 312 | save_state_to_file_or_folder(state, NULL); 313 | } 314 | 315 | void clear_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 316 | action_clear(state); 317 | } 318 | 319 | void copy_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 320 | // Commit a potential paint (e.g. text being written) 321 | commit_state(state); 322 | clipboard_copy_drawing_area_to_selection(state); 323 | } 324 | 325 | void control_modifier_changed(bool pressed, struct swappy_state *state) { 326 | if (state->temp_paint != NULL) { 327 | switch (state->temp_paint->type) { 328 | case SWAPPY_PAINT_MODE_ELLIPSE: 329 | case SWAPPY_PAINT_MODE_RECTANGLE: 330 | paint_update_temporary_shape( 331 | state, state->temp_paint->content.shape.to.x, 332 | state->temp_paint->content.shape.to.y, pressed); 333 | render_state(state); 334 | break; 335 | default: 336 | break; 337 | } 338 | } 339 | } 340 | 341 | void window_keypress_handler(GtkWidget *widget, GdkEventKey *event, 342 | struct swappy_state *state) { 343 | if (state->temp_paint && state->mode == SWAPPY_PAINT_MODE_TEXT) { 344 | paint_update_temporary_text(state, event); 345 | render_state(state); 346 | return; 347 | } 348 | if (event->state & GDK_CONTROL_MASK) { 349 | switch (event->keyval) { 350 | case GDK_KEY_c: 351 | clipboard_copy_drawing_area_to_selection(state); 352 | break; 353 | case GDK_KEY_s: 354 | save_state_to_file_or_folder(state, NULL); 355 | break; 356 | case GDK_KEY_b: 357 | action_toggle_painting_panel(state, NULL); 358 | break; 359 | case GDK_KEY_w: 360 | gtk_main_quit(); 361 | break; 362 | case GDK_KEY_z: 363 | action_undo(state); 364 | break; 365 | case GDK_KEY_Z: 366 | case GDK_KEY_y: 367 | action_redo(state); 368 | break; 369 | default: 370 | break; 371 | } 372 | } else { 373 | switch (event->keyval) { 374 | case GDK_KEY_Escape: 375 | case GDK_KEY_q: 376 | maybe_save_output_file(state); 377 | gtk_main_quit(); 378 | break; 379 | case GDK_KEY_b: 380 | switch_mode_to_brush(state); 381 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->brush), true); 382 | break; 383 | case GDK_KEY_t: 384 | switch_mode_to_text(state); 385 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->text), true); 386 | break; 387 | case GDK_KEY_r: 388 | switch_mode_to_rectangle(state); 389 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->rectangle), 390 | true); 391 | break; 392 | case GDK_KEY_o: 393 | switch_mode_to_ellipse(state); 394 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->ellipse), 395 | true); 396 | break; 397 | case GDK_KEY_a: 398 | switch_mode_to_arrow(state); 399 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->arrow), true); 400 | break; 401 | case GDK_KEY_d: 402 | switch_mode_to_blur(state); 403 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blur), true); 404 | break; 405 | case GDK_KEY_k: 406 | action_clear(state); 407 | break; 408 | case GDK_KEY_R: 409 | action_update_color_state(state, 1, 0, 0, 1, false); 410 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->red), true); 411 | break; 412 | case GDK_KEY_G: 413 | action_update_color_state(state, 0, 1, 0, 1, false); 414 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->green), true); 415 | break; 416 | case GDK_KEY_B: 417 | action_update_color_state(state, 0, 0, 1, 1, false); 418 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blue), true); 419 | break; 420 | case GDK_KEY_C: 421 | action_set_color_from_custom(state); 422 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->custom), 423 | true); 424 | break; 425 | case GDK_KEY_minus: 426 | action_stroke_size_decrease(state); 427 | break; 428 | case GDK_KEY_equal: 429 | action_stroke_size_reset(state); 430 | break; 431 | case GDK_KEY_plus: 432 | action_stroke_size_increase(state); 433 | break; 434 | case GDK_KEY_Control_L: 435 | control_modifier_changed(true, state); 436 | break; 437 | case GDK_KEY_f: 438 | action_fill_shape_toggle(state, NULL); 439 | break; 440 | default: 441 | break; 442 | } 443 | } 444 | } 445 | 446 | void window_keyrelease_handler(GtkWidget *widget, GdkEventKey *event, 447 | struct swappy_state *state) { 448 | if (event->state & GDK_CONTROL_MASK) { 449 | switch (event->keyval) { 450 | case GDK_KEY_Control_L: 451 | control_modifier_changed(false, state); 452 | break; 453 | default: 454 | break; 455 | } 456 | } else { 457 | switch (event->keyval) { 458 | default: 459 | break; 460 | } 461 | } 462 | } 463 | 464 | gboolean window_delete_handler(GtkWidget *widget, GdkEvent *event, 465 | struct swappy_state *state) { 466 | gtk_main_quit(); 467 | return FALSE; 468 | } 469 | 470 | void pane_toggled_handler(GtkWidget *widget, struct swappy_state *state) { 471 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(widget); 472 | gboolean toggled = gtk_toggle_button_get_active(button); 473 | action_toggle_painting_panel(state, &toggled); 474 | } 475 | 476 | void undo_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 477 | action_undo(state); 478 | } 479 | 480 | void redo_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 481 | action_redo(state); 482 | } 483 | 484 | gboolean draw_area_handler(GtkWidget *widget, cairo_t *cr, 485 | struct swappy_state *state) { 486 | GtkAllocation *alloc = g_new(GtkAllocation, 1); 487 | gtk_widget_get_allocation(widget, alloc); 488 | 489 | GdkPixbuf *image = state->original_image; 490 | gint image_width = gdk_pixbuf_get_width(image); 491 | gint image_height = gdk_pixbuf_get_height(image); 492 | double scale_x = (double)alloc->width / image_width; 493 | double scale_y = (double)alloc->height / image_height; 494 | 495 | cairo_scale(cr, scale_x, scale_y); 496 | cairo_set_source_surface(cr, state->rendering_surface, 0, 0); 497 | cairo_paint(cr); 498 | 499 | return FALSE; 500 | } 501 | 502 | gboolean draw_area_configure_handler(GtkWidget *widget, 503 | GdkEventConfigure *event, 504 | struct swappy_state *state) { 505 | g_debug("received configure_event callback"); 506 | 507 | pixbuf_scale_surface_from_widget(state, widget); 508 | 509 | render_state(state); 510 | 511 | return TRUE; 512 | } 513 | 514 | void draw_area_button_press_handler(GtkWidget *widget, GdkEventButton *event, 515 | struct swappy_state *state) { 516 | gdouble x, y; 517 | 518 | screen_coordinates_to_image_coordinates(state, event->x, event->y, &x, &y); 519 | 520 | if (event->button == 1) { 521 | switch (state->mode) { 522 | case SWAPPY_PAINT_MODE_BLUR: 523 | case SWAPPY_PAINT_MODE_BRUSH: 524 | case SWAPPY_PAINT_MODE_RECTANGLE: 525 | case SWAPPY_PAINT_MODE_ELLIPSE: 526 | case SWAPPY_PAINT_MODE_ARROW: 527 | case SWAPPY_PAINT_MODE_TEXT: 528 | paint_add_temporary(state, x, y, state->mode); 529 | render_state(state); 530 | update_ui_undo_redo(state); 531 | break; 532 | default: 533 | return; 534 | } 535 | } 536 | } 537 | void draw_area_motion_notify_handler(GtkWidget *widget, GdkEventMotion *event, 538 | struct swappy_state *state) { 539 | gdouble x, y; 540 | 541 | screen_coordinates_to_image_coordinates(state, event->x, event->y, &x, &y); 542 | 543 | GdkDisplay *display = gdk_display_get_default(); 544 | GdkWindow *window = event->window; 545 | GdkCursor *crosshair = gdk_cursor_new_for_display(display, GDK_CROSSHAIR); 546 | gdk_window_set_cursor(window, crosshair); 547 | 548 | gboolean is_button1_pressed = event->state & GDK_BUTTON1_MASK; 549 | gboolean is_control_pressed = event->state & GDK_CONTROL_MASK; 550 | 551 | switch (state->mode) { 552 | case SWAPPY_PAINT_MODE_BLUR: 553 | case SWAPPY_PAINT_MODE_BRUSH: 554 | case SWAPPY_PAINT_MODE_RECTANGLE: 555 | case SWAPPY_PAINT_MODE_ELLIPSE: 556 | case SWAPPY_PAINT_MODE_ARROW: 557 | if (is_button1_pressed) { 558 | paint_update_temporary_shape(state, x, y, is_control_pressed); 559 | render_state(state); 560 | } 561 | break; 562 | case SWAPPY_PAINT_MODE_TEXT: 563 | if (is_button1_pressed) { 564 | paint_update_temporary_text_clip(state, x, y); 565 | render_state(state); 566 | } 567 | break; 568 | default: 569 | return; 570 | } 571 | g_object_unref(crosshair); 572 | } 573 | void draw_area_button_release_handler(GtkWidget *widget, GdkEventButton *event, 574 | struct swappy_state *state) { 575 | if (!(event->state & GDK_BUTTON1_MASK)) { 576 | return; 577 | } 578 | 579 | switch (state->mode) { 580 | case SWAPPY_PAINT_MODE_BLUR: 581 | case SWAPPY_PAINT_MODE_BRUSH: 582 | case SWAPPY_PAINT_MODE_RECTANGLE: 583 | case SWAPPY_PAINT_MODE_ELLIPSE: 584 | case SWAPPY_PAINT_MODE_ARROW: 585 | commit_state(state); 586 | break; 587 | case SWAPPY_PAINT_MODE_TEXT: 588 | if (state->temp_paint && !state->temp_paint->can_draw) { 589 | paint_free(state->temp_paint); 590 | state->temp_paint = NULL; 591 | } 592 | break; 593 | default: 594 | return; 595 | } 596 | } 597 | 598 | void color_red_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 599 | action_update_color_state(state, 1, 0, 0, 1, false); 600 | } 601 | 602 | void color_green_clicked_handler(GtkWidget *widget, 603 | struct swappy_state *state) { 604 | action_update_color_state(state, 0, 1, 0, 1, false); 605 | } 606 | 607 | void color_blue_clicked_handler(GtkWidget *widget, struct swappy_state *state) { 608 | action_update_color_state(state, 0, 0, 1, 1, false); 609 | } 610 | 611 | void color_custom_clicked_handler(GtkWidget *widget, 612 | struct swappy_state *state) { 613 | action_set_color_from_custom(state); 614 | } 615 | 616 | void color_custom_color_set_handler(GtkWidget *widget, 617 | struct swappy_state *state) { 618 | action_set_color_from_custom(state); 619 | } 620 | 621 | void stroke_size_decrease_handler(GtkWidget *widget, 622 | struct swappy_state *state) { 623 | action_stroke_size_decrease(state); 624 | } 625 | 626 | void stroke_size_reset_handler(GtkWidget *widget, struct swappy_state *state) { 627 | action_stroke_size_reset(state); 628 | } 629 | void stroke_size_increase_handler(GtkWidget *widget, 630 | struct swappy_state *state) { 631 | action_stroke_size_increase(state); 632 | } 633 | 634 | void text_size_decrease_handler(GtkWidget *widget, struct swappy_state *state) { 635 | action_text_size_decrease(state); 636 | } 637 | void text_size_reset_handler(GtkWidget *widget, struct swappy_state *state) { 638 | action_text_size_reset(state); 639 | } 640 | void text_size_increase_handler(GtkWidget *widget, struct swappy_state *state) { 641 | action_text_size_increase(state); 642 | } 643 | 644 | void fill_shape_toggled_handler(GtkWidget *widget, struct swappy_state *state) { 645 | GtkToggleButton *button = GTK_TOGGLE_BUTTON(widget); 646 | gboolean toggled = gtk_toggle_button_get_active(button); 647 | action_fill_shape_toggle(state, &toggled); 648 | } 649 | 650 | static void compute_window_size_and_scaling_factor(struct swappy_state *state) { 651 | GdkRectangle workarea = {0}; 652 | GdkDisplay *display = gdk_display_get_default(); 653 | GdkWindow *window = gtk_widget_get_window(GTK_WIDGET(state->ui->window)); 654 | GdkMonitor *monitor = gdk_display_get_monitor_at_window(display, window); 655 | gdk_monitor_get_workarea(monitor, &workarea); 656 | 657 | g_assert(workarea.width > 0); 658 | g_assert(workarea.height > 0); 659 | 660 | if (state->window) { 661 | g_free(state->window); 662 | state->window = NULL; 663 | } 664 | 665 | state->window = g_new(struct swappy_box, 1); 666 | state->window->x = workarea.x; 667 | state->window->y = workarea.y; 668 | 669 | double threshold = 0.75; 670 | double scaling_factor = 1.0; 671 | 672 | int image_width = gdk_pixbuf_get_width(state->original_image); 673 | int image_height = gdk_pixbuf_get_height(state->original_image); 674 | 675 | int max_width = workarea.width * threshold; 676 | int max_height = workarea.height * threshold; 677 | 678 | g_info("size of image: %ux%u", image_width, image_height); 679 | g_info("size of monitor at window: %ux%u", workarea.width, workarea.height); 680 | g_info("maxium size allowed for window: %ux%u", max_width, max_height); 681 | 682 | int scaled_width = image_width; 683 | int scaled_height = image_height; 684 | 685 | double scaling_factor_width = (double)max_width / image_width; 686 | double scaling_factor_height = (double)max_height / image_height; 687 | 688 | if (scaling_factor_height < 1.0 || scaling_factor_width < 1.0) { 689 | scaling_factor = MIN(scaling_factor_width, scaling_factor_height); 690 | scaled_width = image_width * scaling_factor; 691 | scaled_height = image_height * scaling_factor; 692 | g_info("rendering area will be scaled by a factor of: %.2lf", 693 | scaling_factor); 694 | } 695 | 696 | state->scaling_factor = scaling_factor; 697 | state->window->width = scaled_width; 698 | state->window->height = scaled_height; 699 | 700 | g_info("size of window to render: %ux%u", state->window->width, 701 | state->window->height); 702 | } 703 | 704 | static void apply_css(GtkWidget *widget, GtkStyleProvider *provider) { 705 | gtk_style_context_add_provider(gtk_widget_get_style_context(widget), provider, 706 | GTK_STYLE_PROVIDER_PRIORITY_APPLICATION); 707 | if (GTK_IS_CONTAINER(widget)) { 708 | gtk_container_forall(GTK_CONTAINER(widget), (GtkCallback)apply_css, 709 | provider); 710 | } 711 | } 712 | 713 | static bool load_css(struct swappy_state *state) { 714 | GtkCssProvider *provider = gtk_css_provider_new(); 715 | gtk_css_provider_load_from_resource(provider, 716 | "/me/jtheoof/swappy/style/swappy.css"); 717 | apply_css(GTK_WIDGET(state->ui->window), GTK_STYLE_PROVIDER(provider)); 718 | g_object_unref(provider); 719 | return true; 720 | } 721 | 722 | static bool load_layout(struct swappy_state *state) { 723 | GError *error = NULL; 724 | // init color 725 | GdkRGBA color; 726 | 727 | /* Construct a GtkBuilder instance and load our UI description */ 728 | GtkBuilder *builder = gtk_builder_new(); 729 | 730 | // Set translation domain for the application based on `src/po/meson.build` 731 | gtk_builder_set_translation_domain(builder, GETTEXT_PACKAGE); 732 | 733 | if (gtk_builder_add_from_resource(builder, "/me/jtheoof/swappy/swappy.glade", 734 | &error) == 0) { 735 | g_printerr("Error loading file: %s", error->message); 736 | g_clear_error(&error); 737 | return false; 738 | } 739 | 740 | gtk_builder_connect_signals(builder, state); 741 | 742 | GtkWindow *window = 743 | GTK_WINDOW(gtk_builder_get_object(builder, "paint-window")); 744 | 745 | g_signal_connect(window, "destroy", G_CALLBACK(on_destroy), state); 746 | 747 | state->ui->panel_toggle_button = 748 | GTK_TOGGLE_BUTTON(gtk_builder_get_object(builder, "btn-toggle-panel")); 749 | 750 | state->ui->undo = GTK_BUTTON(gtk_builder_get_object(builder, "undo-button")); 751 | state->ui->redo = GTK_BUTTON(gtk_builder_get_object(builder, "redo-button")); 752 | 753 | GtkWidget *area = 754 | GTK_WIDGET(gtk_builder_get_object(builder, "painting-area")); 755 | 756 | state->ui->painting_box = 757 | GTK_BOX(gtk_builder_get_object(builder, "painting-box")); 758 | GtkRadioButton *brush = 759 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "brush")); 760 | GtkRadioButton *text = 761 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "text")); 762 | GtkRadioButton *rectangle = 763 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "rectangle")); 764 | GtkRadioButton *ellipse = 765 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "ellipse")); 766 | GtkRadioButton *arrow = 767 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "arrow")); 768 | GtkRadioButton *blur = 769 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "blur")); 770 | 771 | state->ui->red = 772 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-red-button")); 773 | state->ui->green = 774 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-green-button")); 775 | state->ui->blue = 776 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-blue-button")); 777 | state->ui->custom = 778 | GTK_RADIO_BUTTON(gtk_builder_get_object(builder, "color-custom-button")); 779 | state->ui->color = 780 | GTK_COLOR_BUTTON(gtk_builder_get_object(builder, "custom-color-button")); 781 | 782 | state->ui->line_size = 783 | GTK_BUTTON(gtk_builder_get_object(builder, "stroke-size-button")); 784 | state->ui->text_size = 785 | GTK_BUTTON(gtk_builder_get_object(builder, "text-size-button")); 786 | 787 | state->ui->fill_shape = GTK_TOGGLE_BUTTON( 788 | gtk_builder_get_object(builder, "fill-shape-toggle-button")); 789 | 790 | gdk_rgba_parse(&color, state->config->custom_color); 791 | gtk_color_chooser_set_rgba(GTK_COLOR_CHOOSER(state->ui->color), &color); 792 | 793 | state->ui->brush = brush; 794 | state->ui->text = text; 795 | state->ui->rectangle = rectangle; 796 | state->ui->ellipse = ellipse; 797 | state->ui->arrow = arrow; 798 | state->ui->blur = blur; 799 | state->ui->area = area; 800 | state->ui->window = window; 801 | 802 | compute_window_size_and_scaling_factor(state); 803 | gtk_widget_set_size_request(area, state->window->width, 804 | state->window->height); 805 | action_toggle_painting_panel(state, &state->config->show_panel); 806 | 807 | g_object_unref(G_OBJECT(builder)); 808 | 809 | return true; 810 | } 811 | 812 | static void set_paint_mode(struct swappy_state *state) { 813 | switch (state->mode) { 814 | case SWAPPY_PAINT_MODE_BRUSH: 815 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->brush), true); 816 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 817 | break; 818 | case SWAPPY_PAINT_MODE_TEXT: 819 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->text), true); 820 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 821 | break; 822 | case SWAPPY_PAINT_MODE_RECTANGLE: 823 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->rectangle), 824 | true); 825 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true); 826 | break; 827 | case SWAPPY_PAINT_MODE_ELLIPSE: 828 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->ellipse), true); 829 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), true); 830 | break; 831 | case SWAPPY_PAINT_MODE_ARROW: 832 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->arrow), true); 833 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 834 | break; 835 | case SWAPPY_PAINT_MODE_BLUR: 836 | gtk_toggle_button_set_active(GTK_TOGGLE_BUTTON(state->ui->blur), true); 837 | gtk_widget_set_sensitive(GTK_WIDGET(state->ui->fill_shape), false); 838 | break; 839 | default: 840 | break; 841 | } 842 | } 843 | 844 | static bool init_gtk_window(struct swappy_state *state) { 845 | if (!state->original_image) { 846 | g_critical("original image not loaded"); 847 | return false; 848 | } 849 | 850 | if (!load_layout(state)) { 851 | return false; 852 | } 853 | 854 | if (!load_css(state)) { 855 | return false; 856 | } 857 | 858 | set_paint_mode(state); 859 | 860 | update_ui_stroke_size_widget(state); 861 | update_ui_text_size_widget(state); 862 | update_ui_undo_redo(state); 863 | update_ui_panel_toggle_button(state); 864 | update_ui_fill_shape_toggle_button(state); 865 | 866 | return true; 867 | } 868 | 869 | static gboolean has_option_file(struct swappy_state *state) { 870 | return (state->file_str != NULL); 871 | } 872 | 873 | static gboolean is_file_from_stdin(const char *file) { 874 | return (strcmp(file, "-") == 0); 875 | } 876 | 877 | static void init_settings(struct swappy_state *state) { 878 | state->settings.r = 1; 879 | state->settings.g = 0; 880 | state->settings.b = 0; 881 | state->settings.a = 1; 882 | state->settings.w = state->config->line_size; 883 | state->settings.t = state->config->text_size; 884 | state->mode = state->config->paint_mode; 885 | } 886 | 887 | static gint command_line_handler(GtkApplication *app, 888 | GApplicationCommandLine *cmdline, 889 | struct swappy_state *state) { 890 | config_load(state); 891 | init_settings(state); 892 | 893 | if (has_option_file(state)) { 894 | if (is_file_from_stdin(state->file_str)) { 895 | char *temp_file_str = file_dump_stdin_into_a_temp_file(); 896 | state->temp_file_str = temp_file_str; 897 | } 898 | 899 | if (!pixbuf_init_from_file(state)) { 900 | return EXIT_FAILURE; 901 | } 902 | } 903 | 904 | if (!init_gtk_window(state)) { 905 | return EXIT_FAILURE; 906 | } 907 | 908 | return EXIT_SUCCESS; 909 | } 910 | 911 | // Print version and quit 912 | gboolean callback_on_flag(const gchar *option_name, const gchar *value, 913 | gpointer data, GError **error) { 914 | if (!strcmp(option_name, "-v") || !strcmp(option_name, "--version")) { 915 | printf("swappy version %s\n", SWAPPY_VERSION); 916 | exit(0); 917 | } 918 | return TRUE; 919 | } 920 | 921 | bool application_init(struct swappy_state *state) { 922 | // Callback function for flags 923 | gboolean (*GOptionArgFunc)(const gchar *option_name, const gchar *value, 924 | gpointer data, GError **error); 925 | GOptionArgFunc = &callback_on_flag; 926 | 927 | const GOptionEntry cli_options[] = { 928 | { 929 | .long_name = "file", 930 | .short_name = 'f', 931 | .arg = G_OPTION_ARG_STRING, 932 | .arg_data = &state->file_str, 933 | .description = "Load a file at a specific path", 934 | }, 935 | { 936 | .long_name = "output-file", 937 | .short_name = 'o', 938 | .arg = G_OPTION_ARG_STRING, 939 | .arg_data = &state->output_file, 940 | .description = "Print the final surface to the given file when " 941 | "exiting, use - to print to stdout", 942 | }, 943 | { 944 | .long_name = "version", 945 | .short_name = 'v', 946 | .flags = G_OPTION_FLAG_NO_ARG, 947 | .arg = G_OPTION_ARG_CALLBACK, 948 | .arg_data = GOptionArgFunc, 949 | .description = "Print version and quit", 950 | }, 951 | {NULL}}; 952 | 953 | state->app = gtk_application_new("me.jtheoof.swappy", 954 | G_APPLICATION_HANDLES_COMMAND_LINE); 955 | 956 | if (state->app == NULL) { 957 | g_critical("cannot create gtk application"); 958 | return false; 959 | } 960 | 961 | g_application_add_main_option_entries(G_APPLICATION(state->app), cli_options); 962 | 963 | state->ui = g_new(struct swappy_state_ui, 1); 964 | state->ui->panel_toggled = false; 965 | 966 | g_signal_connect(state->app, "command-line", G_CALLBACK(command_line_handler), 967 | state); 968 | 969 | return true; 970 | } 971 | 972 | int application_run(struct swappy_state *state) { 973 | return g_application_run(G_APPLICATION(state->app), state->argc, state->argv); 974 | } 975 | -------------------------------------------------------------------------------- /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("paint_mode: %d", config->paint_mode); 23 | g_info("early_exit: %d", config->early_exit); 24 | g_info("fill_shape: %d", config->fill_shape); 25 | g_info("auto_save: %d", config->auto_save); 26 | g_info("custom_color: %s", config->custom_color); 27 | } 28 | 29 | static char *get_default_save_dir() { 30 | static const char *storage_paths[] = { 31 | "$XDG_DESKTOP_DIR", 32 | "$XDG_CONFIG_HOME/Desktop", 33 | "$HOME/Desktop", 34 | "$HOME", 35 | }; 36 | 37 | for (size_t i = 0; i < sizeof(storage_paths) / sizeof(char *); ++i) { 38 | wordexp_t p; 39 | if (wordexp(storage_paths[i], &p, 0) == 0) { 40 | char *path = g_strdup(p.we_wordv[0]); 41 | wordfree(&p); 42 | if (path && folder_exists(path)) { 43 | return path; 44 | } 45 | g_free(path); 46 | } 47 | } 48 | 49 | return NULL; 50 | } 51 | 52 | static char *get_config_file() { 53 | static const char *storage_paths[] = { 54 | "$XDG_CONFIG_HOME/swappy/config", 55 | "$HOME/.config/swappy/config", 56 | }; 57 | 58 | for (size_t i = 0; i < sizeof(storage_paths) / sizeof(char *); ++i) { 59 | wordexp_t p; 60 | if (wordexp(storage_paths[i], &p, 0) == 0) { 61 | char *path = g_strdup(p.we_wordv[0]); 62 | wordfree(&p); 63 | if (path && file_exists(path)) { 64 | return path; 65 | } 66 | g_free(path); 67 | } 68 | } 69 | 70 | return NULL; 71 | } 72 | 73 | static void load_config_from_file(struct swappy_config *config, 74 | const char *file) { 75 | GKeyFile *gkf; 76 | const gchar *group = "Default"; 77 | gchar *save_dir = NULL; 78 | gchar *save_filename_format = NULL; 79 | gboolean show_panel; 80 | gchar *save_dir_expanded = NULL; 81 | guint64 line_size, text_size; 82 | gchar *text_font = NULL; 83 | gchar *paint_mode = NULL; 84 | gboolean early_exit; 85 | gboolean fill_shape; 86 | gboolean auto_save; 87 | gchar *custom_color = NULL; 88 | GError *error = NULL; 89 | 90 | if (file == NULL) { 91 | return; 92 | } 93 | 94 | gkf = g_key_file_new(); 95 | 96 | if (!g_key_file_load_from_file(gkf, file, G_KEY_FILE_NONE, NULL)) { 97 | g_warning("could not read config file %s", file); 98 | g_key_file_free(gkf); 99 | return; 100 | } 101 | 102 | save_dir = g_key_file_get_string(gkf, group, "save_dir", &error); 103 | 104 | if (error == NULL) { 105 | wordexp_t p; 106 | if (wordexp(save_dir, &p, 0) == 0) { 107 | save_dir_expanded = g_strdup(p.we_wordv[0]); 108 | wordfree(&p); 109 | if (!save_dir_expanded || !folder_exists(save_dir_expanded)) { 110 | g_info("save_dir: attempting to create non-existent directory '%s'", 111 | save_dir_expanded); 112 | if (g_mkdir_with_parents(save_dir_expanded, 0755)) { 113 | g_warning("save_dir: failed to create '%s'", save_dir_expanded); 114 | } 115 | } 116 | 117 | g_free(save_dir); 118 | g_free(config->save_dir); 119 | config->save_dir = save_dir_expanded; 120 | } 121 | } else { 122 | g_info("save_dir is missing in %s (%s)", file, error->message); 123 | g_error_free(error); 124 | error = NULL; 125 | } 126 | 127 | save_filename_format = 128 | g_key_file_get_string(gkf, group, "save_filename_format", &error); 129 | 130 | if (error == NULL) { 131 | config->save_filename_format = save_filename_format; 132 | } else { 133 | g_info("save_filename_format is missing in %s (%s)", file, error->message); 134 | g_error_free(error); 135 | error = NULL; 136 | } 137 | 138 | line_size = g_key_file_get_uint64(gkf, group, "line_size", &error); 139 | 140 | if (error == NULL) { 141 | if (line_size >= SWAPPY_LINE_SIZE_MIN && 142 | line_size <= SWAPPY_LINE_SIZE_MAX) { 143 | config->line_size = line_size; 144 | } else { 145 | g_warning("line_size is not a valid value: %" PRIu64 146 | " - see man page for details", 147 | line_size); 148 | } 149 | } else { 150 | g_info("line_size is missing in %s (%s)", file, error->message); 151 | g_error_free(error); 152 | error = NULL; 153 | } 154 | 155 | text_size = g_key_file_get_uint64(gkf, group, "text_size", &error); 156 | 157 | if (error == NULL) { 158 | if (text_size >= SWAPPY_TEXT_SIZE_MIN && 159 | text_size <= SWAPPY_TEXT_SIZE_MAX) { 160 | config->text_size = text_size; 161 | } else { 162 | g_warning("text_size is not a valid value: %" PRIu64 163 | " - see man page for details", 164 | text_size); 165 | } 166 | } else { 167 | g_info("text_size is missing in %s (%s)", file, error->message); 168 | g_error_free(error); 169 | error = NULL; 170 | } 171 | 172 | text_font = g_key_file_get_string(gkf, group, "text_font", &error); 173 | 174 | if (error == NULL) { 175 | g_free(config->text_font); 176 | config->text_font = text_font; 177 | } else { 178 | g_info("text_font is missing in %s (%s)", file, error->message); 179 | g_error_free(error); 180 | error = NULL; 181 | } 182 | 183 | show_panel = g_key_file_get_boolean(gkf, group, "show_panel", &error); 184 | 185 | if (error == NULL) { 186 | config->show_panel = show_panel; 187 | } else { 188 | g_info("show_panel is missing in %s (%s)", file, error->message); 189 | g_error_free(error); 190 | error = NULL; 191 | } 192 | 193 | early_exit = g_key_file_get_boolean(gkf, group, "early_exit", &error); 194 | 195 | if (error == NULL) { 196 | config->early_exit = early_exit; 197 | } else { 198 | g_info("early_exit is missing in %s (%s)", file, error->message); 199 | g_error_free(error); 200 | error = NULL; 201 | } 202 | 203 | paint_mode = g_key_file_get_string(gkf, group, "paint_mode", &error); 204 | 205 | if (error == NULL) { 206 | if (g_ascii_strcasecmp(paint_mode, "brush") == 0) { 207 | config->paint_mode = SWAPPY_PAINT_MODE_BRUSH; 208 | } else if (g_ascii_strcasecmp(paint_mode, "text") == 0) { 209 | config->paint_mode = SWAPPY_PAINT_MODE_TEXT; 210 | } else if (g_ascii_strcasecmp(paint_mode, "rectangle") == 0) { 211 | config->paint_mode = SWAPPY_PAINT_MODE_RECTANGLE; 212 | } else if (g_ascii_strcasecmp(paint_mode, "ellipse") == 0) { 213 | config->paint_mode = SWAPPY_PAINT_MODE_ELLIPSE; 214 | } else if (g_ascii_strcasecmp(paint_mode, "arrow") == 0) { 215 | config->paint_mode = SWAPPY_PAINT_MODE_ARROW; 216 | } else if (g_ascii_strcasecmp(paint_mode, "blur") == 0) { 217 | config->paint_mode = SWAPPY_PAINT_MODE_BLUR; 218 | } else { 219 | g_warning( 220 | "paint_mode is not a valid value: %s - see man page for details", 221 | paint_mode); 222 | } 223 | } else { 224 | g_info("paint_mode is missing in %s (%s)", file, error->message); 225 | g_error_free(error); 226 | error = NULL; 227 | } 228 | 229 | fill_shape = g_key_file_get_boolean(gkf, group, "fill_shape", &error); 230 | 231 | if (error == NULL) { 232 | config->fill_shape = fill_shape; 233 | } else { 234 | g_info("fill_shape is missing in %s (%s)", file, error->message); 235 | g_error_free(error); 236 | error = NULL; 237 | } 238 | 239 | auto_save = g_key_file_get_boolean(gkf, group, "auto_save", &error); 240 | 241 | if (error == NULL) { 242 | config->auto_save = auto_save; 243 | } else { 244 | g_info("auto_save is missing in %s (%s)", file, error->message); 245 | g_error_free(error); 246 | error = NULL; 247 | } 248 | 249 | custom_color = g_key_file_get_string(gkf, group, "custom_color", &error); 250 | 251 | if (error == NULL) { 252 | config->custom_color = custom_color; 253 | } else { 254 | g_info("custom_color is missing in %s (%s)", file, error->message); 255 | g_error_free(error); 256 | error = NULL; 257 | } 258 | 259 | g_key_file_free(gkf); 260 | } 261 | 262 | static void load_default_config(struct swappy_config *config) { 263 | if (config == NULL) { 264 | return; 265 | } 266 | 267 | config->save_dir = get_default_save_dir(); 268 | config->save_filename_format = g_strdup(CONFIG_SAVE_FILENAME_FORMAT_DEFAULT); 269 | config->line_size = CONFIG_LINE_SIZE_DEFAULT; 270 | config->text_font = g_strdup(CONFIG_TEXT_FONT_DEFAULT); 271 | config->text_size = CONFIG_TEXT_SIZE_DEFAULT; 272 | config->show_panel = CONFIG_SHOW_PANEL_DEFAULT; 273 | config->paint_mode = CONFIG_PAINT_MODE_DEFAULT; 274 | config->early_exit = CONFIG_EARLY_EXIT_DEFAULT; 275 | config->fill_shape = CONFIG_FILL_SHAPE_DEFAULT; 276 | config->auto_save = CONFIG_AUTO_SAVE_DEFAULT; 277 | config->custom_color = g_strdup(CONFIG_CUSTOM_COLOR_DEFAULT); 278 | } 279 | 280 | void config_load(struct swappy_state *state) { 281 | struct swappy_config *config = g_new(struct swappy_config, 1); 282 | 283 | load_default_config(config); 284 | 285 | char *file = get_config_file(); 286 | if (file) { 287 | load_config_from_file(config, file); 288 | } else { 289 | g_info("could not find swappy config file, using defaults"); 290 | } 291 | 292 | config->config_file = file; 293 | state->config = config; 294 | 295 | print_config(state->config); 296 | } 297 | 298 | void config_free(struct swappy_state *state) { 299 | if (state->config) { 300 | g_free(state->config->config_file); 301 | g_free(state->config->save_dir); 302 | g_free(state->config->save_filename_format); 303 | g_free(state->config->text_font); 304 | g_free(state->config->custom_color); 305 | g_free(state->config); 306 | state->config = NULL; 307 | } 308 | } 309 | -------------------------------------------------------------------------------- /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 | 5 | #include "util.h" 6 | 7 | static void cursor_move_backward(struct swappy_paint_text *text) { 8 | if (text->cursor > 0) { 9 | text->cursor--; 10 | } 11 | } 12 | 13 | static void cursor_move_forward(struct swappy_paint_text *text) { 14 | if (text->cursor < g_utf8_strlen(text->text, -1)) { 15 | text->cursor++; 16 | } 17 | } 18 | 19 | void paint_free(gpointer data) { 20 | struct swappy_paint *paint = (struct swappy_paint *)data; 21 | 22 | if (paint == NULL) { 23 | return; 24 | } 25 | 26 | switch (paint->type) { 27 | case SWAPPY_PAINT_MODE_BLUR: 28 | if (paint->content.blur.surface) { 29 | cairo_surface_destroy(paint->content.blur.surface); 30 | } 31 | break; 32 | case SWAPPY_PAINT_MODE_BRUSH: 33 | g_list_free_full(paint->content.brush.points, g_free); 34 | break; 35 | case SWAPPY_PAINT_MODE_TEXT: 36 | g_free(paint->content.text.text); 37 | g_free(paint->content.text.font); 38 | break; 39 | default: 40 | break; 41 | } 42 | g_free(paint); 43 | } 44 | 45 | void paint_free_list(GList **list) { 46 | if (*list) { 47 | g_list_free_full(*list, paint_free); 48 | *list = NULL; 49 | } 50 | } 51 | 52 | void paint_free_all(struct swappy_state *state) { 53 | paint_free_list(&state->paints); 54 | paint_free_list(&state->redo_paints); 55 | paint_free(state->temp_paint); 56 | state->temp_paint = NULL; 57 | } 58 | 59 | void paint_add_temporary(struct swappy_state *state, double x, double y, 60 | enum swappy_paint_type type) { 61 | struct swappy_paint *paint = g_new(struct swappy_paint, 1); 62 | struct swappy_point *point; 63 | 64 | double r = state->settings.r; 65 | double g = state->settings.g; 66 | double b = state->settings.b; 67 | double a = state->settings.a; 68 | double w = state->settings.w; 69 | double t = state->settings.t; 70 | 71 | paint->type = type; 72 | paint->is_committed = false; 73 | 74 | g_debug("adding temporary paint at: %.2lfx%.2lf", x, y); 75 | 76 | if (state->temp_paint) { 77 | if (type == SWAPPY_PAINT_MODE_TEXT) { 78 | paint_commit_temporary(state); 79 | } else { 80 | paint_free(state->temp_paint); 81 | state->temp_paint = NULL; 82 | } 83 | } 84 | 85 | switch (type) { 86 | case SWAPPY_PAINT_MODE_BLUR: 87 | paint->can_draw = false; 88 | 89 | paint->content.blur.from.x = x; 90 | paint->content.blur.from.y = y; 91 | paint->content.blur.surface = NULL; 92 | break; 93 | case SWAPPY_PAINT_MODE_BRUSH: 94 | paint->can_draw = true; 95 | 96 | paint->content.brush.r = r; 97 | paint->content.brush.g = g; 98 | paint->content.brush.b = b; 99 | paint->content.brush.a = a; 100 | paint->content.brush.w = w; 101 | 102 | point = g_new(struct swappy_point, 1); 103 | point->x = x; 104 | point->y = y; 105 | 106 | paint->content.brush.points = g_list_prepend(NULL, point); 107 | break; 108 | case SWAPPY_PAINT_MODE_RECTANGLE: 109 | case SWAPPY_PAINT_MODE_ELLIPSE: 110 | case SWAPPY_PAINT_MODE_ARROW: 111 | paint->can_draw = false; // need `to` vector 112 | 113 | paint->content.shape.from.x = x; 114 | paint->content.shape.from.y = y; 115 | paint->content.shape.r = r; 116 | paint->content.shape.g = g; 117 | paint->content.shape.b = b; 118 | paint->content.shape.a = a; 119 | paint->content.shape.w = w; 120 | paint->content.shape.type = type; 121 | if (state->config->fill_shape) 122 | paint->content.shape.operation = SWAPPY_PAINT_SHAPE_OPERATION_FILL; 123 | else 124 | paint->content.shape.operation = SWAPPY_PAINT_SHAPE_OPERATION_STROKE; 125 | break; 126 | case SWAPPY_PAINT_MODE_TEXT: 127 | paint->can_draw = false; 128 | 129 | paint->content.text.from.x = x; 130 | paint->content.text.from.y = y; 131 | paint->content.text.r = r; 132 | paint->content.text.g = g; 133 | paint->content.text.b = b; 134 | paint->content.text.a = a; 135 | paint->content.text.s = t; 136 | paint->content.text.font = g_strdup(state->config->text_font); 137 | paint->content.text.cursor = 0; 138 | paint->content.text.mode = SWAPPY_TEXT_MODE_EDIT; 139 | paint->content.text.text = g_new(gchar, 1); 140 | paint->content.text.text[0] = '\0'; 141 | break; 142 | 143 | default: 144 | g_info("unable to add temporary paint: %d", type); 145 | break; 146 | } 147 | 148 | state->temp_paint = paint; 149 | } 150 | 151 | void paint_update_temporary_shape(struct swappy_state *state, double x, 152 | double y, gboolean is_control_pressed) { 153 | struct swappy_paint *paint = state->temp_paint; 154 | struct swappy_point *point; 155 | GList *points; 156 | 157 | if (!paint) { 158 | return; 159 | } 160 | 161 | switch (paint->type) { 162 | case SWAPPY_PAINT_MODE_BLUR: 163 | paint->can_draw = true; 164 | paint->content.blur.to.x = x; 165 | paint->content.blur.to.y = y; 166 | break; 167 | case SWAPPY_PAINT_MODE_BRUSH: 168 | points = paint->content.brush.points; 169 | point = g_new(struct swappy_point, 1); 170 | point->x = x; 171 | point->y = y; 172 | 173 | paint->content.brush.points = g_list_prepend(points, point); 174 | break; 175 | case SWAPPY_PAINT_MODE_RECTANGLE: 176 | case SWAPPY_PAINT_MODE_ELLIPSE: 177 | paint->can_draw = true; // all set 178 | 179 | paint->content.shape.should_center_at_from = is_control_pressed; 180 | paint->content.shape.to.x = x; 181 | paint->content.shape.to.y = y; 182 | break; 183 | case SWAPPY_PAINT_MODE_ARROW: 184 | paint->can_draw = true; // all set 185 | 186 | paint->content.shape.to.x = x; 187 | paint->content.shape.to.y = y; 188 | break; 189 | default: 190 | g_info("unable to update temporary paint when type is: %d", paint->type); 191 | break; 192 | } 193 | } 194 | 195 | void paint_update_temporary_text(struct swappy_state *state, 196 | GdkEventKey *event) { 197 | struct swappy_paint *paint = state->temp_paint; 198 | struct swappy_paint_text *text; 199 | char *new_text; 200 | char buffer[32]; 201 | guint32 unicode; 202 | 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 | 210 | switch (event->keyval) { 211 | case GDK_KEY_Escape: 212 | paint_commit_temporary(state); 213 | break; 214 | case GDK_KEY_BackSpace: 215 | if (g_utf8_strlen(text->text, -1) > 0) { 216 | new_text = string_remove_at(text->text, text->cursor - 1); 217 | g_free(text->text); 218 | text->text = new_text; 219 | cursor_move_backward(text); 220 | } 221 | break; 222 | case GDK_KEY_Delete: 223 | if (g_utf8_strlen(text->text, -1) > 0) { 224 | new_text = string_remove_at(text->text, text->cursor); 225 | g_free(text->text); 226 | text->text = new_text; 227 | } 228 | break; 229 | case GDK_KEY_Left: 230 | cursor_move_backward(text); 231 | break; 232 | case GDK_KEY_Right: 233 | cursor_move_forward(text); 234 | break; 235 | default: 236 | unicode = gdk_keyval_to_unicode(event->keyval); 237 | if (unicode != 0) { 238 | int ll = g_unichar_to_utf8(unicode, buffer); 239 | buffer[ll] = '\0'; 240 | char *new_text = 241 | string_insert_chars_at(text->text, buffer, text->cursor); 242 | g_free(text->text); 243 | text->text = new_text; 244 | text->cursor++; 245 | } 246 | break; 247 | } 248 | } 249 | 250 | void paint_update_temporary_text_clip(struct swappy_state *state, gdouble x, 251 | gdouble y) { 252 | struct swappy_paint *paint = state->temp_paint; 253 | 254 | if (!paint) { 255 | return; 256 | } 257 | 258 | g_assert(paint->type == SWAPPY_PAINT_MODE_TEXT); 259 | 260 | paint->can_draw = true; 261 | paint->content.text.to.x = x; 262 | paint->content.text.to.y = y; 263 | } 264 | 265 | void paint_commit_temporary(struct swappy_state *state) { 266 | struct swappy_paint *paint = state->temp_paint; 267 | 268 | if (!paint) { 269 | return; 270 | } 271 | 272 | switch (paint->type) { 273 | case SWAPPY_PAINT_MODE_TEXT: 274 | if (g_utf8_strlen(paint->content.text.text, -1) == 0) { 275 | paint->can_draw = false; 276 | } 277 | paint->content.text.mode = SWAPPY_TEXT_MODE_DONE; 278 | break; 279 | default: 280 | break; 281 | } 282 | 283 | if (!paint->can_draw) { 284 | paint_free(paint); 285 | } else { 286 | paint->is_committed = true; 287 | state->paints = g_list_prepend(state->paints, paint); 288 | } 289 | 290 | // Set the temporary paint to NULL but keep the content in memory 291 | // because it's now part of the GList. 292 | state->temp_paint = NULL; 293 | } 294 | -------------------------------------------------------------------------------- /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 | gboolean has_alpha = gdk_pixbuf_get_has_alpha(image); 97 | cairo_format_t format = has_alpha ? CAIRO_FORMAT_ARGB32 : CAIRO_FORMAT_RGB24; 98 | gint image_width = gdk_pixbuf_get_width(image); 99 | gint image_height = gdk_pixbuf_get_height(image); 100 | 101 | cairo_surface_t *original_image_surface = 102 | cairo_image_surface_create(format, image_width, image_height); 103 | 104 | if (!original_image_surface) { 105 | g_error("unable to create cairo original surface from pixbuf"); 106 | goto finish; 107 | } else { 108 | cairo_t *cr; 109 | cr = cairo_create(original_image_surface); 110 | gdk_cairo_set_source_pixbuf(cr, image, 0, 0); 111 | cairo_paint(cr); 112 | cairo_destroy(cr); 113 | } 114 | 115 | cairo_surface_t *rendering_surface = 116 | cairo_image_surface_create(format, image_width, image_height); 117 | 118 | if (!rendering_surface) { 119 | g_error("unable to create rendering surface"); 120 | goto finish; 121 | } 122 | 123 | g_info("size of area to render: %ux%u", alloc->width, alloc->height); 124 | 125 | finish: 126 | if (state->original_image_surface) { 127 | cairo_surface_destroy(state->original_image_surface); 128 | state->original_image_surface = NULL; 129 | } 130 | state->original_image_surface = original_image_surface; 131 | 132 | if (state->rendering_surface) { 133 | cairo_surface_destroy(state->rendering_surface); 134 | state->rendering_surface = NULL; 135 | } 136 | state->rendering_surface = rendering_surface; 137 | 138 | g_free(alloc); 139 | } 140 | 141 | void pixbuf_free(struct swappy_state *state) { 142 | if (G_IS_OBJECT(state->original_image)) { 143 | g_object_unref(state->original_image); 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /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: 2022-11-18 16:07-0500\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:456 21 | msgid "Line Width" 22 | msgstr "Linienstärke" 23 | 24 | #: res/swappy.glade:526 25 | msgid "Text Size" 26 | msgstr "Textgröße" 27 | 28 | #: res/swappy.glade:592 29 | msgid "Fill shape" 30 | msgstr "" 31 | 32 | #: res/swappy.glade:597 33 | msgid "Toggle shape filling" 34 | msgstr "" 35 | 36 | #: res/swappy.glade:671 37 | msgid "Toggle Paint Panel" 38 | msgstr "Farbtafel umschalten" 39 | 40 | #: res/swappy.glade:697 41 | msgid "Undo Last Paint" 42 | msgstr "Letzte Bemalung rückgängig machen" 43 | 44 | #: res/swappy.glade:716 45 | msgid "Redo Previous Paint" 46 | msgstr "Vorherige Bemalung wiederherstellen" 47 | 48 | #: res/swappy.glade:735 49 | msgid "Clear Paints" 50 | msgstr "Bemalung löschen" 51 | 52 | #: res/swappy.glade:763 53 | msgid "Copy Surface" 54 | msgstr "Fläche kopieren" 55 | 56 | #: res/swappy.glade:779 57 | msgid "Save Surface" 58 | msgstr "Fläche speichern" 59 | -------------------------------------------------------------------------------- /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: 2022-11-18 16:07-0500\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:456 21 | msgid "Line Width" 22 | msgstr "Line Width" 23 | 24 | #: res/swappy.glade:526 25 | msgid "Text Size" 26 | msgstr "Text Size" 27 | 28 | #: res/swappy.glade:592 29 | msgid "Fill shape" 30 | msgstr "Fill shape" 31 | 32 | #: res/swappy.glade:597 33 | msgid "Toggle shape filling" 34 | msgstr "Toggle shape filling" 35 | 36 | #: res/swappy.glade:671 37 | msgid "Toggle Paint Panel" 38 | msgstr "Toggle Paint Panel" 39 | 40 | #: res/swappy.glade:697 41 | msgid "Undo Last Paint" 42 | msgstr "Undo Last Paint" 43 | 44 | #: res/swappy.glade:716 45 | msgid "Redo Previous Paint" 46 | msgstr "Redo Previous Paint" 47 | 48 | #: res/swappy.glade:735 49 | msgid "Clear Paints" 50 | msgstr "Clear Paints" 51 | 52 | #: res/swappy.glade:763 53 | msgid "Copy Surface" 54 | msgstr "Copy Surface" 55 | 56 | #: res/swappy.glade:779 57 | msgid "Save Surface" 58 | msgstr "Save Surface" 59 | -------------------------------------------------------------------------------- /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: 2022-11-18 16:07-0500\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:456 21 | msgid "Line Width" 22 | msgstr "Epaisseur de ligne" 23 | 24 | #: res/swappy.glade:526 25 | msgid "Text Size" 26 | msgstr "Taille du texte" 27 | 28 | #: res/swappy.glade:592 29 | msgid "Fill shape" 30 | msgstr "Remplir la forme" 31 | 32 | #: res/swappy.glade:597 33 | msgid "Toggle shape filling" 34 | msgstr "Activer/Désactiver le remplissage de forme" 35 | 36 | #: res/swappy.glade:671 37 | msgid "Toggle Paint Panel" 38 | msgstr "Afficher/Cacher le panneau de peinture" 39 | 40 | #: res/swappy.glade:697 41 | msgid "Undo Last Paint" 42 | msgstr "Annuler la dernière peinture" 43 | 44 | #: res/swappy.glade:716 45 | msgid "Redo Previous Paint" 46 | msgstr "Rétablir la dernière peinture" 47 | 48 | #: res/swappy.glade:735 49 | msgid "Clear Paints" 50 | msgstr "Supprimer les peintures" 51 | 52 | #: res/swappy.glade:763 53 | msgid "Copy Surface" 54 | msgstr "Copier la surface" 55 | 56 | #: res/swappy.glade:779 57 | msgid "Save Surface" 58 | msgstr "Sauvegarder la surface" 59 | -------------------------------------------------------------------------------- /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: 2022-11-18 16:07-0500\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:456 22 | msgid "Line Width" 23 | msgstr "Espessura da linha" 24 | 25 | #: res/swappy.glade:526 26 | msgid "Text Size" 27 | msgstr "Tamanho do texto" 28 | 29 | #: res/swappy.glade:592 30 | msgid "Fill shape" 31 | msgstr "" 32 | 33 | #: res/swappy.glade:597 34 | msgid "Toggle shape filling" 35 | msgstr "" 36 | 37 | #: res/swappy.glade:671 38 | msgid "Toggle Paint Panel" 39 | msgstr "Alternar painel de pintura" 40 | 41 | #: res/swappy.glade:697 42 | msgid "Undo Last Paint" 43 | msgstr "Desfazer última pintura" 44 | 45 | #: res/swappy.glade:716 46 | msgid "Redo Previous Paint" 47 | msgstr "Refazer pintura anterior" 48 | 49 | #: res/swappy.glade:735 50 | msgid "Clear Paints" 51 | msgstr "Limpar pinturas" 52 | 53 | #: res/swappy.glade:763 54 | msgid "Copy Surface" 55 | msgstr "Copiar superfície" 56 | 57 | #: res/swappy.glade:779 58 | msgid "Save Surface" 59 | msgstr "Salvar superfície" 60 | -------------------------------------------------------------------------------- /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: 2022-11-18 16:07-0500\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:456 21 | msgid "Line Width" 22 | msgstr "" 23 | 24 | #: res/swappy.glade:526 25 | msgid "Text Size" 26 | msgstr "" 27 | 28 | #: res/swappy.glade:592 29 | msgid "Fill shape" 30 | msgstr "" 31 | 32 | #: res/swappy.glade:597 33 | msgid "Toggle shape filling" 34 | msgstr "" 35 | 36 | #: res/swappy.glade:671 37 | msgid "Toggle Paint Panel" 38 | msgstr "" 39 | 40 | #: res/swappy.glade:697 41 | msgid "Undo Last Paint" 42 | msgstr "" 43 | 44 | #: res/swappy.glade:716 45 | msgid "Redo Previous Paint" 46 | msgstr "" 47 | 48 | #: res/swappy.glade:735 49 | msgid "Clear Paints" 50 | msgstr "" 51 | 52 | #: res/swappy.glade:763 53 | msgid "Copy Surface" 54 | msgstr "" 55 | 56 | #: res/swappy.glade:779 57 | msgid "Save Surface" 58 | msgstr "" 59 | -------------------------------------------------------------------------------- /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: 2022-11-18 16:07-0500\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:456 21 | msgid "Line Width" 22 | msgstr "Çizgi Genişliği" 23 | 24 | #: res/swappy.glade:526 25 | msgid "Text Size" 26 | msgstr "Metin Boyutu" 27 | 28 | #: res/swappy.glade:592 29 | msgid "Fill shape" 30 | msgstr "Şekli Doldur" 31 | 32 | #: res/swappy.glade:597 33 | msgid "Toggle shape filling" 34 | msgstr "Şekil Doldurmayı Aç/Kapat" 35 | 36 | #: res/swappy.glade:671 37 | msgid "Toggle Paint Panel" 38 | msgstr "Boyama Panelini Aç/Kapat" 39 | 40 | #: res/swappy.glade:697 41 | msgid "Undo Last Paint" 42 | msgstr "Son Boyamayı Geri Al" 43 | 44 | #: res/swappy.glade:716 45 | msgid "Redo Previous Paint" 46 | msgstr "Önceki Boyamayı Tekrarla" 47 | 48 | #: res/swappy.glade:735 49 | msgid "Clear Paints" 50 | msgstr "Boyamaları Temizle" 51 | 52 | #: res/swappy.glade:763 53 | msgid "Copy Surface" 54 | msgstr "Yüzeyi Kopyala" 55 | 56 | #: res/swappy.glade:779 57 | msgid "Save Surface" 58 | msgstr "Yüzeyi Kaydet" 59 | -------------------------------------------------------------------------------- /src/po/zh_CN.po: -------------------------------------------------------------------------------- 1 | 2 | # English translations for swappy package. 3 | # Copyright (C) 2020 THE swappy'S COPYRIGHT HOLDER 4 | # This file is distributed under the same license as the swappy package. 5 | # Automatically generated, 2020. 6 | # 7 | msgid "" 8 | msgstr "" 9 | "Project-Id-Version: swappy\n" 10 | "Report-Msgid-Bugs-To: \n" 11 | "POT-Creation-Date: 2022-11-18 16:07-0500\n" 12 | "PO-Revision-Date: 2020-06-21 21:57-0400\n" 13 | "Last-Translator: Automatically generated\n" 14 | "Language-Team: none\n" 15 | "Language: zh_CN\n" 16 | "MIME-Version: 1.0\n" 17 | "Content-Type: text/plain; charset=UTF-8\n" 18 | "Content-Transfer-Encoding: 8bit\n" 19 | "Plural-Forms: nplurals=1; plural=0;\n" 20 | 21 | #: res/swappy.glade:456 22 | msgid "Line Width" 23 | msgstr "行宽" 24 | 25 | #: res/swappy.glade:526 26 | msgid "Text Size" 27 | msgstr "文本大小" 28 | 29 | #: res/swappy.glade:592 30 | msgid "Fill shape" 31 | msgstr "填充" 32 | 33 | #: res/swappy.glade:597 34 | msgid "Toggle shape filling" 35 | msgstr "切换填充状态" 36 | 37 | #: res/swappy.glade:671 38 | msgid "Toggle Paint Panel" 39 | msgstr "切换绘图板状态" 40 | 41 | #: res/swappy.glade:697 42 | msgid "Undo Last Paint" 43 | msgstr "撤销" 44 | 45 | #: res/swappy.glade:716 46 | msgid "Redo Previous Paint" 47 | msgstr "恢复" 48 | 49 | #: res/swappy.glade:735 50 | msgid "Clear Paints" 51 | msgstr "清除绘图" 52 | 53 | #: res/swappy.glade:763 54 | msgid "Copy Surface" 55 | msgstr "复制" 56 | 57 | #: res/swappy.glade:779 58 | msgid "Save Surface" 59 | msgstr "保存" 60 | -------------------------------------------------------------------------------- /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 | char pango_font[255]; 184 | double x = fmin(text.from.x, text.to.x); 185 | double y = fmin(text.from.y, text.to.y); 186 | double w = fabs(text.from.x - text.to.x); 187 | double h = fabs(text.from.y - text.to.y); 188 | 189 | cairo_surface_t *surface = 190 | cairo_image_surface_create(CAIRO_FORMAT_ARGB32, w, h); 191 | cairo_t *crt = cairo_create(surface); 192 | 193 | pango_layout_t *layout = pango_cairo_create_layout(crt); 194 | pango_layout_set_text(layout, text.text, -1); 195 | g_snprintf(pango_font, 255, "%s %d", text.font, (int)text.s); 196 | pango_font_description_t *desc = 197 | pango_font_description_from_string(pango_font); 198 | pango_layout_set_width(layout, pango_units_from_double(w)); 199 | pango_layout_set_font_description(layout, desc); 200 | pango_layout_set_wrap(layout, PANGO_WRAP_WORD_CHAR); 201 | pango_font_description_free(desc); 202 | 203 | if (text.mode == SWAPPY_TEXT_MODE_EDIT) { 204 | pango_rectangle_t strong_pos; 205 | struct swappy_box cursor_box; 206 | cairo_set_source_rgba(cr, 0.5, 0.5, 0.5, 0.3); 207 | cairo_set_line_width(cr, 5); 208 | cairo_rectangle(cr, x, y, w, h); 209 | cairo_stroke(cr); 210 | glong bytes_til_cursor = string_get_nb_bytes_until(text.text, text.cursor); 211 | pango_layout_get_cursor_pos(layout, bytes_til_cursor, &strong_pos, NULL); 212 | convert_pango_rectangle_to_swappy_box(strong_pos, &cursor_box); 213 | cairo_move_to(crt, cursor_box.x, cursor_box.y); 214 | cairo_set_source_rgba(crt, 0.3, 0.3, 0.3, 1); 215 | cairo_line_to(crt, cursor_box.x, cursor_box.y + cursor_box.height); 216 | cairo_stroke(crt); 217 | } 218 | 219 | cairo_rectangle(crt, 0, 0, w, h); 220 | cairo_set_source_rgba(crt, text.r, text.g, text.b, text.a); 221 | cairo_move_to(crt, 0, 0); 222 | pango_cairo_show_layout(crt, layout); 223 | 224 | cairo_set_source_surface(cr, surface, x, y); 225 | cairo_paint(cr); 226 | 227 | cairo_destroy(crt); 228 | cairo_surface_destroy(surface); 229 | g_object_unref(layout); 230 | } 231 | 232 | static void render_shape_arrow(cairo_t *cr, struct swappy_paint_shape shape) { 233 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a); 234 | cairo_set_line_width(cr, shape.w); 235 | 236 | double ftx = shape.to.x - shape.from.x; 237 | double fty = shape.to.y - shape.from.y; 238 | double ftn = sqrt(ftx * ftx + fty * fty); 239 | 240 | double r = 20; 241 | double scaling_factor = shape.w / 4; 242 | 243 | double alpha = G_PI / 6; 244 | double ta = 5 * alpha; 245 | double tb = 7 * alpha; 246 | double xa = r * cos(ta); 247 | double ya = r * sin(ta); 248 | double xb = r * cos(tb); 249 | double yb = r * sin(tb); 250 | double xc = ftn - fabs(xa) * scaling_factor; 251 | 252 | if (xc < DBL_EPSILON) { 253 | xc = 0; 254 | } 255 | 256 | if (ftn < DBL_EPSILON) { 257 | return; 258 | } 259 | 260 | double theta = copysign(1.0, fty) * acos(ftx / ftn); 261 | 262 | // Draw line 263 | cairo_save(cr); 264 | cairo_translate(cr, shape.from.x, shape.from.y); 265 | cairo_rotate(cr, theta); 266 | cairo_move_to(cr, 0, 0); 267 | cairo_line_to(cr, xc, 0); 268 | cairo_stroke(cr); 269 | cairo_restore(cr); 270 | 271 | // Draw arrow 272 | cairo_save(cr); 273 | cairo_translate(cr, shape.to.x, shape.to.y); 274 | cairo_rotate(cr, theta); 275 | cairo_scale(cr, scaling_factor, scaling_factor); 276 | cairo_move_to(cr, 0, 0); 277 | cairo_line_to(cr, xa, ya); 278 | cairo_line_to(cr, xb, yb); 279 | cairo_line_to(cr, 0, 0); 280 | cairo_fill(cr); 281 | cairo_restore(cr); 282 | } 283 | 284 | static void render_shape_ellipse(cairo_t *cr, struct swappy_paint_shape shape) { 285 | double x = fabs(shape.from.x - shape.to.x); 286 | double y = fabs(shape.from.y - shape.to.y); 287 | 288 | double n = sqrt(x * x + y * y); 289 | 290 | double xc, yc, r; 291 | 292 | if (shape.should_center_at_from) { 293 | xc = shape.from.x; 294 | yc = shape.from.y; 295 | 296 | r = n; 297 | } else { 298 | xc = shape.from.x + ((shape.to.x - shape.from.x) / 2); 299 | yc = shape.from.y + ((shape.to.y - shape.from.y) / 2); 300 | 301 | r = n / 2; 302 | } 303 | 304 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a); 305 | cairo_set_line_width(cr, shape.w); 306 | 307 | cairo_matrix_t save_matrix; 308 | cairo_get_matrix(cr, &save_matrix); 309 | cairo_translate(cr, xc, yc); 310 | cairo_scale(cr, x / n, y / n); 311 | cairo_arc(cr, 0, 0, r, 0, 2 * G_PI); 312 | cairo_set_matrix(cr, &save_matrix); 313 | 314 | switch (shape.operation) { 315 | case SWAPPY_PAINT_SHAPE_OPERATION_STROKE: 316 | cairo_stroke(cr); 317 | break; 318 | case SWAPPY_PAINT_SHAPE_OPERATION_FILL: 319 | cairo_fill(cr); 320 | break; 321 | default: 322 | cairo_stroke(cr); 323 | break; 324 | } 325 | 326 | cairo_close_path(cr); 327 | } 328 | 329 | static void render_shape_rectangle(cairo_t *cr, 330 | struct swappy_paint_shape shape) { 331 | double x, y, w, h; 332 | 333 | if (shape.should_center_at_from) { 334 | x = shape.from.x - fabs(shape.from.x - shape.to.x); 335 | y = shape.from.y - fabs(shape.from.y - shape.to.y); 336 | w = fabs(shape.from.x - shape.to.x) * 2; 337 | h = fabs(shape.from.y - shape.to.y) * 2; 338 | } else { 339 | x = fmin(shape.from.x, shape.to.x); 340 | y = fmin(shape.from.y, shape.to.y); 341 | w = fabs(shape.from.x - shape.to.x); 342 | h = fabs(shape.from.y - shape.to.y); 343 | } 344 | 345 | cairo_set_source_rgba(cr, shape.r, shape.g, shape.b, shape.a); 346 | cairo_set_line_width(cr, shape.w); 347 | 348 | cairo_rectangle(cr, x, y, w, h); 349 | cairo_close_path(cr); 350 | 351 | switch (shape.operation) { 352 | case SWAPPY_PAINT_SHAPE_OPERATION_STROKE: 353 | cairo_stroke(cr); 354 | break; 355 | case SWAPPY_PAINT_SHAPE_OPERATION_FILL: 356 | cairo_fill(cr); 357 | break; 358 | default: 359 | cairo_stroke(cr); 360 | break; 361 | } 362 | } 363 | 364 | static void render_shape(cairo_t *cr, struct swappy_paint_shape shape) { 365 | cairo_save(cr); 366 | switch (shape.type) { 367 | case SWAPPY_PAINT_MODE_RECTANGLE: 368 | render_shape_rectangle(cr, shape); 369 | break; 370 | case SWAPPY_PAINT_MODE_ELLIPSE: 371 | render_shape_ellipse(cr, shape); 372 | break; 373 | case SWAPPY_PAINT_MODE_ARROW: 374 | render_shape_arrow(cr, shape); 375 | break; 376 | default: 377 | break; 378 | } 379 | cairo_restore(cr); 380 | } 381 | 382 | static void render_background(cairo_t *cr, struct swappy_state *state) { 383 | cairo_set_source_rgb(cr, 0, 0, 0); 384 | cairo_paint(cr); 385 | } 386 | 387 | static void render_blur(cairo_t *cr, struct swappy_paint *paint) { 388 | struct swappy_paint_blur blur = paint->content.blur; 389 | 390 | cairo_surface_t *target = cairo_get_target(cr); 391 | 392 | double x = MIN(blur.from.x, blur.to.x); 393 | double y = MIN(blur.from.y, blur.to.y); 394 | double w = ABS(blur.from.x - blur.to.x); 395 | double h = ABS(blur.from.y - blur.to.y); 396 | 397 | cairo_save(cr); 398 | 399 | if (paint->is_committed) { 400 | // Surface has already been blurred, reuse it in future passes 401 | if (blur.surface) { 402 | cairo_surface_t *surface = blur.surface; 403 | if (surface && cairo_surface_status(surface) == CAIRO_STATUS_SUCCESS) { 404 | cairo_set_source_surface(cr, surface, x, y); 405 | cairo_paint(cr); 406 | } 407 | } else { 408 | // Blur surface and reuse it in future passes 409 | g_info( 410 | "blurring surface on following image coordinates: %.2lf,%.2lf size: " 411 | "%.2lfx%.2lf", 412 | x, y, w, h); 413 | cairo_surface_t *blurred = blur_surface(target, x, y, w, h); 414 | 415 | if (blurred && cairo_surface_status(blurred) == CAIRO_STATUS_SUCCESS) { 416 | cairo_set_source_surface(cr, blurred, x, y); 417 | cairo_paint(cr); 418 | paint->content.blur.surface = blurred; 419 | } 420 | } 421 | } else { 422 | // Blur not committed yet, draw bounding rectangle 423 | struct swappy_paint_shape rect = { 424 | .r = 0, 425 | .g = 0.5, 426 | .b = 1, 427 | .a = 0.5, 428 | .w = 5, 429 | .from = blur.from, 430 | .to = blur.to, 431 | .type = SWAPPY_PAINT_MODE_RECTANGLE, 432 | .operation = SWAPPY_PAINT_SHAPE_OPERATION_FILL, 433 | }; 434 | render_shape_rectangle(cr, rect); 435 | } 436 | 437 | cairo_restore(cr); 438 | } 439 | 440 | static void render_brush(cairo_t *cr, struct swappy_paint_brush brush) { 441 | cairo_set_source_rgba(cr, brush.r, brush.g, brush.b, brush.a); 442 | cairo_set_line_width(cr, brush.w); 443 | cairo_set_line_join(cr, CAIRO_LINE_JOIN_BEVEL); 444 | 445 | guint l = g_list_length(brush.points); 446 | 447 | if (l == 1) { 448 | struct swappy_point *point = g_list_nth_data(brush.points, 0); 449 | cairo_rectangle(cr, point->x, point->y, brush.w, brush.w); 450 | cairo_fill(cr); 451 | } else { 452 | for (GList *elem = brush.points; elem; elem = elem->next) { 453 | struct swappy_point *point = elem->data; 454 | cairo_line_to(cr, point->x, point->y); 455 | } 456 | cairo_stroke(cr); 457 | } 458 | } 459 | 460 | static void render_image(cairo_t *cr, struct swappy_state *state) { 461 | cairo_surface_t *surface = state->original_image_surface; 462 | 463 | cairo_save(cr); 464 | 465 | if (surface && !cairo_surface_status(surface)) { 466 | cairo_set_source_surface(cr, surface, 0, 0); 467 | cairo_paint(cr); 468 | } 469 | 470 | cairo_restore(cr); 471 | } 472 | 473 | static void render_paint(cairo_t *cr, struct swappy_paint *paint) { 474 | if (!paint->can_draw) { 475 | return; 476 | } 477 | switch (paint->type) { 478 | case SWAPPY_PAINT_MODE_BLUR: 479 | render_blur(cr, paint); 480 | break; 481 | case SWAPPY_PAINT_MODE_BRUSH: 482 | render_brush(cr, paint->content.brush); 483 | break; 484 | case SWAPPY_PAINT_MODE_RECTANGLE: 485 | case SWAPPY_PAINT_MODE_ELLIPSE: 486 | case SWAPPY_PAINT_MODE_ARROW: 487 | render_shape(cr, paint->content.shape); 488 | break; 489 | case SWAPPY_PAINT_MODE_TEXT: 490 | render_text(cr, paint->content.text); 491 | break; 492 | default: 493 | g_info("unable to render paint with type: %d", paint->type); 494 | break; 495 | } 496 | } 497 | 498 | static void render_paints(cairo_t *cr, struct swappy_state *state) { 499 | for (GList *elem = g_list_last(state->paints); elem; elem = elem->prev) { 500 | struct swappy_paint *paint = elem->data; 501 | render_paint(cr, paint); 502 | } 503 | 504 | if (state->temp_paint) { 505 | render_paint(cr, state->temp_paint); 506 | } 507 | } 508 | 509 | void render_state(struct swappy_state *state) { 510 | cairo_surface_t *surface = state->rendering_surface; 511 | cairo_t *cr = cairo_create(surface); 512 | 513 | render_background(cr, state); 514 | render_image(cr, state); 515 | render_paints(cr, state); 516 | 517 | cairo_destroy(cr); 518 | 519 | // Drawing is finished, notify the GtkDrawingArea it needs to be redrawn. 520 | gtk_widget_queue_draw(state->ui->area); 521 | } 522 | -------------------------------------------------------------------------------- /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 | gchar *new_str = g_new0(gchar, MAX(str_len, 1)); 9 | gchar *buffer_source = str; 10 | gchar *buffer_copy = new_str; 11 | glong i = 0; 12 | gint bytes; 13 | gunichar c; 14 | 15 | if (pos <= str_len && g_utf8_validate(str, -1, NULL)) { 16 | while (*buffer_source != '\0') { 17 | c = g_utf8_get_char(buffer_source); 18 | buffer_source = g_utf8_next_char(buffer_source); 19 | if (i != pos) { 20 | bytes = g_unichar_to_utf8(c, buffer_copy); 21 | buffer_copy += bytes; 22 | } 23 | i++; 24 | } 25 | } 26 | 27 | return new_str; 28 | } 29 | 30 | gchar *string_insert_chars_at(gchar *str, gchar *chars, glong pos) { 31 | gchar *new_str = NULL; 32 | 33 | if (g_utf8_validate(str, -1, NULL) && g_utf8_validate(chars, -1, NULL) && 34 | pos >= 0 && pos <= g_utf8_strlen(str, -1)) { 35 | gchar *from = g_utf8_substring(str, 0, pos); 36 | gchar *end = g_utf8_offset_to_pointer(str, pos); 37 | 38 | new_str = g_strconcat(from, chars, end, NULL); 39 | 40 | g_free(from); 41 | 42 | } else { 43 | new_str = g_new0(gchar, 1); 44 | } 45 | 46 | return new_str; 47 | } 48 | 49 | glong string_get_nb_bytes_until(gchar *str, glong until) { 50 | glong ret = 0; 51 | if (str) { 52 | gchar *sub = g_utf8_substring(str, 0, until); 53 | ret = strlen(sub); 54 | g_free(sub); 55 | } 56 | 57 | return ret; 58 | } 59 | 60 | void pixel_data_print(guint32 pixel) { 61 | const guint32 r = pixel >> 24 & 0xff; 62 | const guint32 g = pixel >> 16 & 0xff; 63 | const guint32 b = pixel >> 8 & 0xff; 64 | const guint32 a = pixel >> 0 & 0xff; 65 | 66 | g_debug("rgba(%u, %d, %u, %u)", r, g, b, a); 67 | } 68 | -------------------------------------------------------------------------------- /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 | ``` 72 | 73 | - *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 74 | - *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 75 | - *show_panel* is used to toggle the paint panel on or off upon startup 76 | - *line_size* is the default line size (must be between 1 and 50) 77 | - *text_size* is the default text size (must be between 10 and 50) 78 | - *text_font* is the font used to render text, its format is pango friendly 79 | - *paint_mode* is the mode activated at application start (must be one of: brush|text|rectangle|ellipse|arrow|blur, matching is case-insensitive) 80 | - *early_exit* is used to make the application exit after saving the picture or copying it to the clipboard 81 | - *fill_shape* is used to toggle shape filling (for the rectangle and ellipsis tools) on or off upon startup 82 | - *auto_save* is used to toggle auto saving of final buffer to *save_dir* upon exit 83 | - *custom_color* is used to set a default value for the custom color. Accepted 84 | 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) 85 | 86 | 87 | # KEY BINDINGS 88 | 89 | ## LAYOUT 90 | 91 | - *Ctrl+b*: Toggle Paint Panel 92 | 93 | ## PAINT MODE 94 | 95 | - *b*: Switch to Brush 96 | - *t*: Switch to Text 97 | - *r*: Switch to Rectangle 98 | - *o*: Switch to Ellipse 99 | - *a*: Switch to Arrow 100 | - *d*: Switch to Blur (d stands for droplet) 101 | 102 | - *R*: Use Red Color 103 | - *G*: Use Green Color 104 | - *B*: Use Blue Color 105 | - *C*: Use Custom Color 106 | - *Minus*: Reduce Stroke Size 107 | - *Plus*: Increase Stroke Size 108 | - *Equal*: Reset Stroke Size 109 | - *f*: Toggle Shape Filling 110 | - *k*: Clear Paints (cannot be undone) 111 | 112 | ## MODIFIERS 113 | 114 | - *Ctrl*: Center Shape (Rectangle & Ellipse) based on draw start 115 | 116 | ## HEADER BAR 117 | 118 | - *Ctrl+z*: Undo 119 | - *Ctrl+Shift+z* or *Ctrl+y*: Redo 120 | - *Ctrl+s*: Save to file (see man page) 121 | - *Ctrl+c*: Copy to clipboard 122 | - *Escape* or *q* or *Ctrl+w*: Quit swappy 123 | 124 | # AUTHORS 125 | 126 | Written and maintained by jtheoof . See 127 | https://github.com/jtheoof/swappy. 128 | -------------------------------------------------------------------------------- /test/images/large.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/2aa3ae2433ee671ddc73e36ece8598e68f7f3632/test/images/large.png -------------------------------------------------------------------------------- /test/images/passwords.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/2aa3ae2433ee671ddc73e36ece8598e68f7f3632/test/images/passwords.png -------------------------------------------------------------------------------- /test/images/small-blue.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jtheoof/swappy/2aa3ae2433ee671ddc73e36ece8598e68f7f3632/test/images/small-blue.png --------------------------------------------------------------------------------