Compare commits

...
6 Commits
Author SHA1 Message Date
haraldandClaude Opus 5.5 ce3a1f697b chore(release): bump version to 0.39.2
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 19m8s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:57:30 +01:00
haraldandClaude Opus 5.5 e47c2b6f28 build: replace printpdf 0.7 with own PDF writer on lopdf 0.46
printpdf 0.7 pinned lopdf 0.31; newer printpdf versions are a complete
rewrite. The export now uses src/pdf_writer.rs, a small writer on top of
lopdf 0.46 with the subset of the printpdf API the export used: pages with
layers (optional content groups), paths, embedded TrueType fonts (Type0,
Identity-H, ToUnicode), images and bookmarks. Bookmark titles are written
as UTF-16BE directly, so the post-processing step for them is gone.
pdf_merge uses lopdf 0.46 as well.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 16:54:13 +01:00
haraldandClaude Opus 5.5 d610ca019a chore(release): bump version to 0.39.1
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 12m19s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:46:13 +01:00
haraldandClaude Opus 5.5 8034c88f8b fix(windows): don't let Windows Ink report the mouse as a pen (Issue #48)
octotablet's default emulates a pen tool from the mouse on Windows Ink, so
every mouse move counted as pen hover and every click as pen down, while the
regular mouse events were ignored as duplicates.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:44:42 +01:00
haraldandClaude Opus 5.5 1492f8affb chore(release): bump version to 0.39.0
Build & Publish Release / Build Packages & Publish to Gitea (push) Successful in 18m6s
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:16:45 +01:00
haraldandClaude Opus 5.5 e5a8601f87 feat: Windows build and Android groundwork (Issues #48, #42)
Shared groundwork:
- paths.rs: folders for data, settings and downloads per system (Linux XDG
  unchanged, Windows %APPDATA%/%LOCALAPPDATA%, Android app folder)
- Linux-only parts behind cfg: Wayland on-screen keyboard (osk/wayland.rs),
  display backend and app id, the vendored winit touch extensions
- run_with(event_loop) so that other entry points can start the app

Windows (#48):
- cross-compiled with MinGW into a portable ink-notes_<version>_x86_64.exe
  (system DLLs only), no console window in release builds
- file picker: user folders and drives; held notebook locks are detected
  (ERROR_LOCK_VIOLATION)
- updater: replaces the running .exe via rename, active at the next start
- octotablet panicking without the Tablet PC components no longer crashes
  the app
- release: .exe built in CI and part of the signed checksums
- all library tests pass under wine

Android (#42):
- android_main entry point, winit with android-native-activity,
  android_logger; octotablet and arboard left out there
- cargo check and the cdylib build for aarch64-linux-android pass; the window
  surface does not follow the activity lifecycle yet

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-27 15:13:37 +01:00
33 changed files with 2350 additions and 522 deletions

No files matched your search

+2 -1
View File
@@ -35,6 +35,7 @@ jobs:
libxrandr-dev \
libxi-dev \
libxkbcommon-dev \
gcc-mingw-w64-x86-64 \
squashfs-tools \
dpkg-dev \
curl \
@@ -62,7 +63,7 @@ jobs:
cargo clippy --all-targets -- -D warnings
cargo test
- name: Build Release Artifacts (.deb, .AppImage, SHA256SUMS.txt + .minisig)
- name: Build Release Artifacts (.deb, .AppImage, .exe, SHA256SUMS.txt + .minisig)
run: |
chmod +x scripts/*.sh
./scripts/build-release.sh
Generated
+277 -125
View File
@@ -9,7 +9,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "01c0457472c38ea5bd1c3b5ada5e368271cb550be7a4ca4a0b4634e9913f6cc2"
dependencies = [
"ab_glyph_rasterizer",
"owned_ttf_parser 0.25.1",
"owned_ttf_parser",
]
[[package]]
@@ -24,6 +24,17 @@ version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
[[package]]
name = "aes"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32"
dependencies = [
"cipher",
"cpubits",
"cpufeatures 0.3.1",
]
[[package]]
name = "ahash"
version = "0.8.12"
@@ -85,6 +96,24 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04"
[[package]]
name = "android_log-sys"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d"
[[package]]
name = "android_logger"
version = "0.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f"
dependencies = [
"android_log-sys",
"env_logger",
"log",
"once_cell",
]
[[package]]
name = "android_system_properties"
version = "0.1.6"
@@ -201,6 +230,24 @@ dependencies = [
"generic-array",
]
[[package]]
name = "block-buffer"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa"
dependencies = [
"hybrid-array",
]
[[package]]
name = "block-padding"
version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b"
dependencies = [
"hybrid-array",
]
[[package]]
name = "block-sys"
version = "0.2.1"
@@ -241,17 +288,6 @@ dependencies = [
"alloc-stdlib",
]
[[package]]
name = "bstr"
version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bb31b46c14244e20ee9984b11bf5c992b91fb6939fea616e3512c8baecdbe5f"
dependencies = [
"memchr",
"regex-automata",
"serde_core",
]
[[package]]
name = "bumpalo"
version = "3.20.3"
@@ -316,6 +352,15 @@ dependencies = [
"wayland-client",
]
[[package]]
name = "cbc"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896"
dependencies = [
"cipher",
]
[[package]]
name = "cc"
version = "1.6.0"
@@ -352,6 +397,27 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527"
[[package]]
name = "chacha20"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06"
dependencies = [
"cfg-if",
"cpufeatures 0.3.1",
"rand_core",
]
[[package]]
name = "cipher"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
dependencies = [
"crypto-common 0.2.2",
"inout",
]
[[package]]
name = "clipboard-win"
version = "5.4.1"
@@ -436,6 +502,12 @@ dependencies = [
"crossbeam-utils",
]
[[package]]
name = "const-oid"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
[[package]]
name = "core-foundation"
version = "0.9.4"
@@ -482,6 +554,12 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f8f80099a98041a3d1622845c271458a2d73e688351bf3cb999266764b81d48"
[[package]]
name = "cpubits"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
[[package]]
name = "cpufeatures"
version = "0.2.17"
@@ -491,6 +569,15 @@ dependencies = [
"libc",
]
[[package]]
name = "cpufeatures"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566"
dependencies = [
"libc",
]
[[package]]
name = "crc32fast"
version = "1.5.2"
@@ -522,6 +609,15 @@ dependencies = [
"typenum",
]
[[package]]
name = "crypto-common"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
dependencies = [
"hybrid-array",
]
[[package]]
name = "cursor-icon"
version = "1.2.0"
@@ -545,20 +641,25 @@ version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "be1e0bca6c3637f992fc1cc7cbc52a78c1ef6db076dbf1059c4323d6a2048376"
[[package]]
name = "deranged"
version = "0.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
"block-buffer 0.10.4",
"crypto-common 0.1.7",
]
[[package]]
name = "digest"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
dependencies = [
"block-buffer 0.12.1",
"const-oid",
"crypto-common 0.2.2",
]
[[package]]
@@ -612,6 +713,15 @@ version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75b325c5dbd37f80359721ad39aca5a29fb04c89279657cffdda8736d0c0b9d2"
[[package]]
name = "ecb"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac"
dependencies = [
"cipher",
]
[[package]]
name = "ecolor"
version = "0.27.2"
@@ -699,6 +809,15 @@ dependencies = [
"syn 2.0.119",
]
[[package]]
name = "env_logger"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
dependencies = [
"log",
]
[[package]]
name = "epaint"
version = "0.27.2"
@@ -939,6 +1058,7 @@ dependencies = [
"cfg-if",
"libc",
"r-efi 6.0.0",
"rand_core",
]
[[package]]
@@ -948,7 +1068,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
dependencies = [
"color_quant",
"weezl",
"weezl 0.1.12",
]
[[package]]
@@ -1249,6 +1369,15 @@ dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "hybrid-array"
version = "0.4.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17"
dependencies = [
"typenum",
]
[[package]]
name = "icrate"
version = "0.0.4"
@@ -1411,9 +1540,10 @@ dependencies = [
[[package]]
name = "ink-notes"
version = "0.38.0"
version = "0.39.2"
dependencies = [
"ab_glyph",
"android_logger",
"arboard",
"base64",
"bytemuck",
@@ -1426,17 +1556,18 @@ dependencies = [
"hayro",
"hayro-svg",
"image",
"log",
"lopdf",
"minisign-verify",
"naga",
"octotablet",
"pollster",
"printpdf",
"quick-xml",
"resvg",
"semver",
"serde",
"serde_json",
"sha2",
"sha2 0.10.9",
"tempfile",
"ureq",
"url",
@@ -1448,6 +1579,16 @@ dependencies = [
"winit",
]
[[package]]
name = "inout"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f"
dependencies = [
"block-padding",
"hybrid-array",
]
[[package]]
name = "itoa"
version = "1.0.18"
@@ -1592,12 +1733,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4a5ff6bcca6c4867b1c4fd4ef63e4db7436ef363e0ad7531d1558856bae64f4"
[[package]]
name = "linked-hash-map"
version = "0.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
[[package]]
name = "linux-raw-sys"
version = "0.4.15"
@@ -1639,19 +1774,29 @@ checksum = "f9f8bd3e56ce4dfc153cf470fffbfa98c7620958b312ca5c3a4b8d5181fd13c6"
[[package]]
name = "lopdf"
version = "0.31.0"
version = "0.46.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07c8e1b6184b1b32ea5f72f572ebdc40e5da1d2921fa469947ff7c480ad1f85a"
checksum = "d43f64cb8c0d673ff84245a6de9e0172c0cc05dc5dd9d263660b5b4a02b6c4ae"
dependencies = [
"aes",
"bitflags 2.13.2",
"brotli-decompressor",
"cbc",
"ecb",
"encoding_rs",
"flate2",
"getrandom 0.4.3",
"indexmap",
"itoa",
"linked-hash-map",
"log",
"md5",
"pom",
"time",
"weezl",
"md-5",
"nom",
"rand",
"rangemap",
"sha2 0.11.1",
"stringprep",
"thiserror 2.0.21",
"weezl 0.2.1",
]
[[package]]
@@ -1664,10 +1809,14 @@ dependencies = [
]
[[package]]
name = "md5"
version = "0.7.0"
name = "md-5"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "490cc448043f947bae3cbee9c203358d62dbee0db12107a74be5c30ccfd09771"
checksum = "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98"
dependencies = [
"cfg-if",
"digest 0.11.3",
]
[[package]]
name = "memchr"
@@ -1798,10 +1947,13 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2bf50223579dc7cdcfb3bfcacf7069ff68243f8c363f62ffa99cf000a6b9c451"
[[package]]
name = "num-conv"
version = "0.2.2"
name = "nom"
version = "8.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
dependencies = [
"memchr",
]
[[package]]
name = "num-traits"
@@ -1983,22 +2135,13 @@ dependencies = [
"libredox",
]
[[package]]
name = "owned_ttf_parser"
version = "0.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "706de7e2214113d63a8238d1910463cfce781129a6f263d13fdb09ff64355ba4"
dependencies = [
"ttf-parser 0.19.2",
]
[[package]]
name = "owned_ttf_parser"
version = "0.25.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36820e9051aca1014ddc75770aab4d68bc1e9e632f0f5627c4086bc216fb583b"
dependencies = [
"ttf-parser 0.25.1",
"ttf-parser",
]
[[package]]
@@ -2167,15 +2310,6 @@ dependencies = [
"arrayvec",
]
[[package]]
name = "pom"
version = "3.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c972d8f86e943ad532d0b04e8965a749ad1d18bb981a9c7b3ae72fe7fd7744b"
dependencies = [
"bstr",
]
[[package]]
name = "potential_utf"
version = "0.1.6"
@@ -2185,30 +2319,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "powerfmt"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a6394b9e965e73d0a289ee54f589087e2c676aedf60885baf52c76b771e4958"
[[package]]
name = "presser"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8cf8e6a8aa66ce33f63993ffc4ea4271eb5b0530a9002db8455ea6050c77bfa"
[[package]]
name = "printpdf"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c30a4cc87c3ca9a98f4970db158a7153f8d1ec8076e005751173c57836380b1d"
dependencies = [
"js-sys",
"lopdf",
"owned_ttf_parser 0.19.0",
"time",
]
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
@@ -2275,12 +2391,35 @@ version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
[[package]]
name = "rand"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af"
dependencies = [
"chacha20",
"getrandom 0.4.3",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69"
[[package]]
name = "range-alloc"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ca45419789ae5a7899559e9512e58ca889e41f04f1f2445e9f4b290ceccd1d08"
[[package]]
name = "rangemap"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d"
[[package]]
name = "raw-window-handle"
version = "0.5.2"
@@ -2342,12 +2481,6 @@ dependencies = [
"bitflags 2.13.2",
]
[[package]]
name = "regex-automata"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
[[package]]
name = "renderdoc-sys"
version = "1.1.0"
@@ -2573,8 +2706,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
"cpufeatures 0.2.17",
"digest 0.10.7",
]
[[package]]
name = "sha2"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19"
dependencies = [
"cfg-if",
"cpufeatures 0.3.1",
"digest 0.11.3",
]
[[package]]
@@ -2715,6 +2859,17 @@ dependencies = [
"float-cmp",
]
[[package]]
name = "stringprep"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
dependencies = [
"unicode-bidi",
"unicode-normalization",
"unicode-properties",
]
[[package]]
name = "strum"
version = "0.26.3"
@@ -2869,40 +3024,10 @@ dependencies = [
"flate2",
"half",
"quick-error",
"weezl",
"weezl 0.1.12",
"zune-jpeg",
]
[[package]]
name = "time"
version = "0.3.55"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
dependencies = [
"deranged",
"num-conv",
"powerfmt",
"serde_core",
"time-core",
"time-macros",
]
[[package]]
name = "time-core"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
[[package]]
name = "time-macros"
version = "0.2.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
dependencies = [
"num-conv",
"time-core",
]
[[package]]
name = "tiny-skia"
version = "0.11.4"
@@ -2964,6 +3089,12 @@ dependencies = [
"zerovec",
]
[[package]]
name = "tinyvec"
version = "1.13.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee"
[[package]]
name = "tokio"
version = "1.53.2"
@@ -3003,12 +3134,6 @@ dependencies = [
"winnow",
]
[[package]]
name = "ttf-parser"
version = "0.19.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49d64318d8311fc2668e48b63969f4343e0a85c4a109aa8460d6672e364b8bd1"
[[package]]
name = "ttf-parser"
version = "0.25.1"
@@ -3030,12 +3155,33 @@ version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-bidi"
version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
[[package]]
name = "unicode-ident"
version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d245f478577f809a851594d02313b640fb437e0bb33866753cff937863096954"
[[package]]
name = "unicode-normalization"
version = "0.1.26"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d"
dependencies = [
"tinyvec",
]
[[package]]
name = "unicode-properties"
version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
[[package]]
name = "unicode-segmentation"
version = "1.13.3"
@@ -3401,6 +3547,12 @@ version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
[[package]]
name = "weezl"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2"
[[package]]
name = "wgpu"
version = "0.19.4"
+25 -5
View File
@@ -1,6 +1,6 @@
[package]
name = "ink-notes"
version = "0.38.0"
version = "0.39.2"
edition = "2021"
description = "Notizbuch-App für Stift, Touch und Maus unter Linux"
repository = "https://gitea.pansi.eu/harald/ink-notes"
@@ -24,8 +24,8 @@ glam = { version = "0.27", features = ["serde"] }
# Asynchroner Starter für wgpu-Device-Setup
pollster = "0.3"
# PDF-Vektorexport
printpdf = "0.7"
# PDF: writing the export (own writer in src/pdf_writer.rs) and merging imported PDFs
lopdf = { version = "0.46", default-features = false }
serde = { version = "1.0.229", features = ["derive"] }
serde_json = "1.0.151"
@@ -38,11 +38,9 @@ semver = "1.0"
sha2 = "0.10"
ureq = { version = "2.10", default-features = false, features = ["tls", "gzip", "json"] }
url = "2.5"
octotablet = "0.1.0"
# Bilder einfügen: Dekodieren (PNG/JPEG/GIF/WebP/BMP) und Systemzwischenablage
image = { version = "0.25", default-features = false, features = ["png", "jpeg", "gif", "webp", "bmp"] }
arboard = { version = "3", default-features = false, features = ["image-data"] }
minisign-verify = "0.3.0"
fs2 = "0.4"
hayro = "0.8"
@@ -58,11 +56,33 @@ resvg = { version = "0.49", default-features = false, features = ["raster-images
# Text-Werkzeug: Schriftmetriken und Rasterung (Schriften aus egui)
ab_glyph = "0.2"
# Pen input with pressure on Linux (Wayland/X11) and Windows (Windows Ink)
[target.'cfg(not(target_os = "android"))'.dependencies]
octotablet = "0.1.0"
# Systemzwischenablage (Bilder und Text)
arboard = { version = "3", default-features = false, features = ["image-data"] }
# Bildschirmtastatur unter Wayland anfordern (eigenes text-input-Objekt, siehe src/osk.rs)
[target.'cfg(target_os = "linux")'.dependencies]
wayland-client = "0.31"
wayland-backend = { version = "0.3", features = ["client_system"] }
wayland-protocols = { version = "0.31", features = ["client", "unstable"] }
# Android (Issue #42): winit bound to the native activity. The library is built as
# cdylib with `cargo rustc --lib --crate-type cdylib --target aarch64-linux-android`
[target.'cfg(target_os = "android")'.dependencies]
winit = { version = "0.29", features = ["rwh_06", "android-native-activity"] }
android_logger = { version = "0.13", default-features = false }
log = "0.4"
[package.metadata.android]
package = "eu.pansi.inknotes"
apk_name = "ink-notes"
[package.metadata.android.sdk]
min_sdk_version = 26
target_sdk_version = 34
[profile.dev]
opt-level = 1 # Erhöht Rendering-Performance auch im Debug-Modus
+12 -4
View File
@@ -13,7 +13,7 @@
ink-notes speichert Handschrift als Vektoren, erkennt auf Wunsch Formen, kann PDFs zum Beschriften sowie Notizbücher aus Xournal++, Rnote und Saber importieren, Bilder und Textfelder einfügen und alles wieder als PDF, PNG oder SVG exportieren. Gespeichert wird automatisch im Hintergrund.
Getestet wird vor allem unter Zorin OS 18 / Ubuntu 24.04 (Wayland und X11).
Getestet wird vor allem unter Zorin OS 18 / Ubuntu 24.04 (Wayland und X11). Eine Windows-Version gibt es als experimentelles portables Programm.
Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemsprache, umstellen lässt sie sich im ☰-Menü unter „Sprache / Language“.
@@ -87,7 +87,7 @@ Die Oberfläche gibt es auf Deutsch und Englisch; ink-notes folgt der Systemspra
## Installation
Pakete gibt es unter [Releases](https://gitea.pansi.eu/harald/ink-notes/releases) als `.deb` und `.AppImage` (x86_64). `<version>` steht für die Versionsnummer, z. B. `0.11.0`.
Pakete gibt es unter [Releases](https://gitea.pansi.eu/harald/ink-notes/releases) als `.deb`, `.AppImage` und für Windows als `.exe` (x86_64). `<version>` steht für die Versionsnummer, z. B. `0.11.0`.
```bash
# Debian-Paket (Zorin OS 18 / Ubuntu 24.04)
@@ -102,6 +102,8 @@ Hinweise:
- Benötigt `glibc >= 2.39` (Ubuntu 24.04, Zorin OS 18) und einen Grafiktreiber mit Vulkan oder OpenGL.
- Meldet das AppImage einen Fehler zu `libfuse.so.2`, entweder `sudo apt install libfuse2t64` installieren oder mit `--appimage-extract-and-run` starten.
**Windows (experimentell):** `ink-notes_<version>_x86_64.exe` ist ein portables Programm für Windows 10/11 – ohne Installation einfach starten (Windows SmartScreen fragt evtl. einmal nach, da das Programm nicht signiert ist). Stifteingabe mit Druck läuft über Windows Ink (noch nicht auf echter Hardware getestet). Notizbücher und Einstellungen liegen in `%APPDATA%\ink-notes`. Updates ersetzen die `.exe` an Ort und Stelle und gelten ab dem nächsten Start.
---
## Tastenkürzel
@@ -152,6 +154,10 @@ cargo test # Unit- und Integrationstests
Die CI prüft zusätzlich `cargo fmt --check` und `cargo clippy --all-targets -- -D warnings`.
Andere Systeme:
- **Windows** wird unter Linux übersetzt: `scripts/package-windows.sh` (braucht `gcc-mingw-w64-x86-64`); die Tests laufen mit `wine` als Runner (`CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUNNER=wine cargo test --target x86_64-pc-windows-gnu`).
- **Android** (in Arbeit, Issue #42): `cargo check --target aarch64-linux-android` und `cargo rustc --lib --crate-type cdylib --target aarch64-linux-android` mit dem NDK-Compiler in `CC_aarch64_linux_android`. Dort startet die App noch nicht: Die Fensterfläche muss erst dem Lebenszyklus der Activity folgen.
### Projektstruktur
```text
@@ -173,6 +179,7 @@ src/
├── assets.rs # Bilder und PDFs im Notizbuchordner
├── pdf_doc.rs # PDF lesen und rendern (hayro)
├── pdf_merge.rs # Importierte PDF-Seiten beim Export unterlegen
├── pdf_writer.rs # Kleiner PDF-Schreiber für den Export (lopdf)
├── import.rs # Import als neues Notizbuch (Gemeinsames)
├── xopp.rs # Import von Xournal++-/Xournal-Notizbüchern
├── rnote.rs # Import von Rnote-Notizbüchern
@@ -183,11 +190,12 @@ src/
├── disk_watch.rs # Änderungen anderer Geräte übernehmen (Sync-Clients)
├── library.rs # Ordner der Notizbücher (library.json)
├── autosave.rs # Speichern im Hintergrund
├── export.rs # PDF-Export (printpdf)
├── export.rs # PDF-Export
├── page_export.rs # Seite als PNG/SVG, Auswahl als Bild
├── file_picker.rs # Eingebaute Dateiauswahl
├── settings.rs # Einstellungen (~/.config/ink-notes/ui.json)
├── platform.rs # Wayland/X11-Erkennung
├── platform.rs # Wayland/X11-Erkennung, Unterschiede zwischen Linux, Windows und Android
├── paths.rs # Ordner für Daten, Einstellungen und Downloads je System
├── updater.rs # Updates von Gitea (Signatur- und Prüfsummenprüfung)
└── ui.rs # Oberfläche (egui): Werkzeugleiste, Menüs, Dialoge
```
+12 -4
View File
@@ -13,7 +13,7 @@
ink-notes stores handwriting as vectors, can recognize shapes, imports PDFs for annotation as well as Xournal++, Rnote and Saber notebooks, inserts images and text boxes, and exports everything again as PDF, PNG or SVG. Saving happens automatically in the background.
It is mainly tested on Zorin OS 18 / Ubuntu 24.04 (Wayland and X11).
It is mainly tested on Zorin OS 18 / Ubuntu 24.04 (Wayland and X11). A Windows version is available as an experimental portable program.
The interface is available in English and German; ink-notes follows the system language, and you can switch it in the ☰ menu under "Sprache / Language".
@@ -87,7 +87,7 @@ The interface is available in English and German; ink-notes follows the system l
## Installation
Packages are available under [Releases](https://gitea.pansi.eu/harald/ink-notes/releases) as `.deb` and `.AppImage` (x86_64). `<version>` stands for the version number, e.g. `0.30.2`.
Packages are available under [Releases](https://gitea.pansi.eu/harald/ink-notes/releases) as `.deb`, `.AppImage` and for Windows as `.exe` (x86_64). `<version>` stands for the version number, e.g. `0.30.2`.
```bash
# Debian package (Zorin OS 18 / Ubuntu 24.04)
@@ -102,6 +102,8 @@ Notes:
- Requires `glibc >= 2.39` (Ubuntu 24.04, Zorin OS 18) and a graphics driver with Vulkan or OpenGL.
- If the AppImage reports an error about `libfuse.so.2`, either install `sudo apt install libfuse2t64` or start it with `--appimage-extract-and-run`.
**Windows (experimental):** `ink-notes_<version>_x86_64.exe` is a portable program for Windows 10/11 – no installation, just start it (Windows SmartScreen may ask once, as the program is not signed). Pen input with pressure uses Windows Ink (not yet tested on real hardware). Notebooks and settings are stored in `%APPDATA%\ink-notes`. Updates replace the `.exe` in place and take effect at the next start.
---
## Keyboard shortcuts
@@ -152,6 +154,10 @@ cargo test # unit and integration tests
CI additionally checks `cargo fmt --check` and `cargo clippy --all-targets -- -D warnings`.
Other systems:
- **Windows** is cross-compiled on Linux: `scripts/package-windows.sh` (needs `gcc-mingw-w64-x86-64`); the tests run with `wine` as runner (`CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUNNER=wine cargo test --target x86_64-pc-windows-gnu`).
- **Android** (in progress, Issue #42): `cargo check --target aarch64-linux-android` and `cargo rustc --lib --crate-type cdylib --target aarch64-linux-android` with the NDK compiler in `CC_aarch64_linux_android`. The app does not start there yet: the window surface still has to follow the activity lifecycle.
New code comments and commit messages are written in English; older code and history still contain German.
### Project structure
@@ -175,6 +181,7 @@ src/
├── assets.rs # Images and PDFs in the notebook folder
├── pdf_doc.rs # Reading and rendering PDFs (hayro)
├── pdf_merge.rs # Underlaying imported PDF pages on export
├── pdf_writer.rs # Small PDF writer for the export (lopdf)
├── import.rs # Import as a new notebook (shared parts)
├── xopp.rs # Import of Xournal++/Xournal notebooks
├── rnote.rs # Import of Rnote notebooks
@@ -185,11 +192,12 @@ src/
├── disk_watch.rs # Taking over changes from other devices (sync clients)
├── library.rs # Notebook folders (library.json)
├── autosave.rs # Saving in the background
├── export.rs # PDF export (printpdf)
├── export.rs # PDF export
├── page_export.rs # Page as PNG/SVG, selection as image
├── file_picker.rs # Built-in file picker
├── settings.rs # Settings (~/.config/ink-notes/ui.json)
├── platform.rs # Wayland/X11 detection
├── platform.rs # Wayland/X11 detection, differences between Linux, Windows and Android
├── paths.rs # Folders for data, settings and downloads per system
├── updater.rs # Updates from Gitea (signature and checksum verification)
└── ui.rs # Interface (egui): toolbar, menus, dialogs
```
+2 -2
View File
@@ -189,13 +189,13 @@ Jede Seite hat einen eigenen Verlauf (`history.rs`): Hinzufügen, Entfernen, Ers
## 8. Export
`export.rs` erzeugt das PDF mit `printpdf`. Jede Seite bekommt ihre eigene Größe und diese Ebenen:
`export.rs` erzeugt das PDF mit `pdf_writer.rs`, einem kleinen Schreiber auf Basis von `lopdf` (Ebenen als optionale Inhaltsgruppen, eingebettete Schriften mit ToUnicode-Tabelle, Bilder, Lesezeichen). Jede Seite bekommt ihre eigene Größe und diese Ebenen:
1. **Background:** Karo, Linien oder Punkte in der gewählten Musterfarbe (immer auf weißem Papier).
2. **PDF** (nur im Notfall): importierte PDF-Seite als Bild, falls sie sich nicht als Vektor übernehmen lässt.
3. **Images:** eingefügte Bilder (höchstens 300 dpi).
4. **Text:** Textfelder als echter Text mit eingebetteter Schrift (durchsuchbar, kopierbar). Jedes Feld liegt in einem eigenen Koordinatensystem, das eine Transformationsmatrix auf seine Ecken abbildet.
5. **Ink:** Striche als gefüllte Polygone, Formen als Pfade. Halbtransparente Striche (Textmarker) bekommen einen Grafikzustand mit Deckkraft (`ca`/`CA`); printpdf kann solche Zustände nicht anlegen, deshalb trägt `add_alpha_states` sie nach dem Speichern mit `lopdf` in die Seitenressourcen ein. Jeder Strich ist ein einziger Pfad und deckt sich daher nicht selbst doppelt. Der Umriss eines Strichs wird vor dem Export vereinfacht (`geometry::simplify_closed_outline`, höchstens 0,01 mm vom gezeichneten entfernt): Das Netz hat etwa 16 Punkte je Stiftpunkt, und mit allen dauerte der Export einer vollen Seite Sekunden. `INK_NOTES_EXACT_OUTLINES=1` exportiert wie bisher jeden Punkt.
5. **Ink:** Striche als gefüllte Polygone, Formen als Pfade. Halbtransparente Striche (Textmarker) bekommen einen Grafikzustand mit Deckkraft (`ca`/`CA`); `add_alpha_states` trägt sie nach dem Speichern mit `lopdf` in die Seitenressourcen ein. Jeder Strich ist ein einziger Pfad und deckt sich daher nicht selbst doppelt. Der Umriss eines Strichs wird vor dem Export vereinfacht (`geometry::simplify_closed_outline`, höchstens 0,01 mm vom gezeichneten entfernt): Das Netz hat etwa 16 Punkte je Stiftpunkt, und mit allen dauerte der Export einer vollen Seite Sekunden. `INK_NOTES_EXACT_OUTLINES=1` exportiert wie bisher jeden Punkt.
Importierte PDF-Seiten werden anschließend als Form-XObject unter die Seite gelegt (`pdf_merge.rs`), bleiben also Vektorgrafik. Seitentitel werden zu Lesezeichen.
+2 -2
View File
@@ -189,13 +189,13 @@ Each page has its own history (`history.rs`): adding, removing, replacing and mo
## 8. Export
`export.rs` creates the PDF with `printpdf`. Each page gets its own size and these layers:
`export.rs` creates the PDF with `pdf_writer.rs`, a small writer on top of `lopdf` (layers as optional content groups, embedded fonts with a ToUnicode map, images, bookmarks). Each page gets its own size and these layers:
1. **Background:** squares, lines or dots in the chosen pattern color (always on white paper).
2. **PDF** (fallback only): imported PDF page as an image, if it cannot be taken over as vectors.
3. **Images:** inserted images (at most 300 dpi).
4. **Text:** text boxes as real text with embedded font (searchable, copyable). Each box lives in its own coordinate system that a transformation matrix maps onto its corners.
5. **Ink:** strokes as filled polygons, shapes as paths. Semi-transparent strokes (highlighter) get a graphics state with opacity (`ca`/`CA`); printpdf cannot create such states, so `add_alpha_states` adds them to the page resources with `lopdf` after saving. Each stroke is a single path and therefore does not cover itself twice. The outline of a stroke is simplified before export (`geometry::simplify_closed_outline`, at most 0.01 mm away from the drawn one): the mesh has about 16 points per pen sample, and with all of them a full page took seconds to export. `INK_NOTES_EXACT_OUTLINES=1` exports every point as before.
5. **Ink:** strokes as filled polygons, shapes as paths. Semi-transparent strokes (highlighter) get a graphics state with opacity (`ca`/`CA`); `add_alpha_states` adds them to the page resources with `lopdf` after saving. Each stroke is a single path and therefore does not cover itself twice. The outline of a stroke is simplified before export (`geometry::simplify_closed_outline`, at most 0.01 mm away from the drawn one): the mesh has about 16 points per pen sample, and with all of them a full page took seconds to export. `INK_NOTES_EXACT_OUTLINES=1` exports every point as before.
Imported PDF pages are then placed under the page as a form XObject (`pdf_merge.rs`), so they stay vector graphics. Page titles become bookmarks.
+2
View File
@@ -29,6 +29,8 @@ Dieses Dokument beschreibt, wo und wie ink-notes Notizbücher speichert, und die
~/.config/ink-notes/ui.json ($XDG_CONFIG_HOME) Einstellungen der Oberfläche
```
Unter Windows liegt dieselbe Struktur in `%APPDATA%\ink-notes` (Notizbücher, `recovery`, `ui.json`); heruntergeladene Updates landen in `%LOCALAPPDATA%\ink-notes`.
Der Ordner `notebooks/` lässt sich in den Einstellungen verlegen (`storage_dir` in `ui.json`), z. B. nach `~/Nextcloud/ink-notes`; `recovery/` bleibt, wo es ist.
- **Notizbuch-IDs** werden aus dem Titel und dem Zeitpunkt gebildet (z. B. `mathematik_1775460000`); das erste Notizbuch heißt `default`.
+2
View File
@@ -29,6 +29,8 @@ This document describes where and how ink-notes stores notebooks, and the versio
~/.config/ink-notes/ui.json ($XDG_CONFIG_HOME) interface settings
```
On Windows the same structure lies in `%APPDATA%\ink-notes` (notebooks, `recovery`, `ui.json`); downloaded updates go to `%LOCALAPPDATA%\ink-notes`.
The folder `notebooks/` can be moved elsewhere in the settings (`storage_dir` in `ui.json`), e.g. to `~/Nextcloud/ink-notes`; `recovery/` stays where it is.
- **Notebook IDs** are built from the title and the time (e.g. `mathematik_1775460000`); the first notebook is called `default`.
+15
View File
@@ -84,6 +84,21 @@
</branding>
<content_rating type="oars-1.1" />
<releases>
<release version="0.39.2" date="2026-10-27">
<description>
<p>PDF export now uses its own small PDF writer on lopdf 0.46 instead of printpdf; the exported files look the same.</p>
</description>
</release>
<release version="0.39.1" date="2026-10-27">
<description>
<p>Windows: the mouse works again; Windows Ink no longer reports it as a pen.</p>
</description>
</release>
<release version="0.39.0" date="2026-10-27">
<description>
<p>First experimental Windows version as a portable program. Internal groundwork for Android; nothing changes on Linux.</p>
</description>
</release>
<release version="0.38.0" date="2026-10-27">
<description>
<p>The notebooks can be stored in a folder synced by Nextcloud, Google Drive, Syncthing or Dropbox. Changes from other devices are taken over automatically, conflict copies become pages of their own, and a warning appears when a notebook is open on another device.</p>
+390
View File
@@ -38,6 +38,19 @@
"dest": "cargo/vendor/adler2-2.0.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/aes/aes-0.9.3.crate",
"sha256": "35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32",
"dest": "cargo/vendor/aes-0.9.3"
},
{
"type": "inline",
"contents": "{\"package\": \"35f0f96ce78e38c3dc6d8948aa8163d06385be74000f3c7a95bf1eef35d3ea32\", \"files\": {}}",
"dest": "cargo/vendor/aes-0.9.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -116,6 +129,32 @@
"dest": "cargo/vendor/android-properties-0.2.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/android_log-sys/android_log-sys-0.3.2.crate",
"sha256": "84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d",
"dest": "cargo/vendor/android_log-sys-0.3.2"
},
{
"type": "inline",
"contents": "{\"package\": \"84521a3cf562bc62942e294181d9eef17eb38ceb8c68677bc49f144e4c3d4f8d\", \"files\": {}}",
"dest": "cargo/vendor/android_log-sys-0.3.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/android_logger/android_logger-0.13.3.crate",
"sha256": "c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f",
"dest": "cargo/vendor/android_logger-0.13.3"
},
{
"type": "inline",
"contents": "{\"package\": \"c494134f746c14dc653a35a4ea5aca24ac368529da5370ecf41fe0341c35772f\", \"files\": {}}",
"dest": "cargo/vendor/android_logger-0.13.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -311,6 +350,32 @@
"dest": "cargo/vendor/block-buffer-0.10.4",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/block-buffer/block-buffer-0.12.1.crate",
"sha256": "d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa",
"dest": "cargo/vendor/block-buffer-0.12.1"
},
{
"type": "inline",
"contents": "{\"package\": \"d2f6c7dbe95a6ed67ad9f18e57daf93a2f034c524b99fd2b76d18fdfeb6660aa\", \"files\": {}}",
"dest": "cargo/vendor/block-buffer-0.12.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/block-padding/block-padding-0.4.2.crate",
"sha256": "710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b",
"dest": "cargo/vendor/block-padding-0.4.2"
},
{
"type": "inline",
"contents": "{\"package\": \"710f1dd022ef4e93f8a438b4ba958de7f64308434fa6a87104481645cc30068b\", \"files\": {}}",
"dest": "cargo/vendor/block-padding-0.4.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -467,6 +532,19 @@
"dest": "cargo/vendor/calloop-wayland-source-0.2.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/cbc/cbc-0.2.1.crate",
"sha256": "ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896",
"dest": "cargo/vendor/cbc-0.2.1"
},
{
"type": "inline",
"contents": "{\"package\": \"ce2dc9ee5f88d11e0beb842c88b33c8a5cf0d1329c4b19494af42b07dbfe8896\", \"files\": {}}",
"dest": "cargo/vendor/cbc-0.2.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -532,6 +610,32 @@
"dest": "cargo/vendor/cfg_aliases-0.2.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/chacha20/chacha20-0.10.2.crate",
"sha256": "65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06",
"dest": "cargo/vendor/chacha20-0.10.2"
},
{
"type": "inline",
"contents": "{\"package\": \"65c35e4b699c7e15ccbe7ee35c005e4fc0a278d22238a2857e6ce2dadeda1b06\", \"files\": {}}",
"dest": "cargo/vendor/chacha20-0.10.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/cipher/cipher-0.5.2.crate",
"sha256": "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c",
"dest": "cargo/vendor/cipher-0.5.2"
},
{
"type": "inline",
"contents": "{\"package\": \"e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c\", \"files\": {}}",
"dest": "cargo/vendor/cipher-0.5.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -649,6 +753,19 @@
"dest": "cargo/vendor/concurrent-queue-2.5.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/const-oid/const-oid-0.10.2.crate",
"sha256": "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c",
"dest": "cargo/vendor/const-oid-0.10.2"
},
{
"type": "inline",
"contents": "{\"package\": \"a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c\", \"files\": {}}",
"dest": "cargo/vendor/const-oid-0.10.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -714,6 +831,19 @@
"dest": "cargo/vendor/core_detect-1.0.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/cpubits/cpubits-0.1.1.crate",
"sha256": "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae",
"dest": "cargo/vendor/cpubits-0.1.1"
},
{
"type": "inline",
"contents": "{\"package\": \"15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae\", \"files\": {}}",
"dest": "cargo/vendor/cpubits-0.1.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -727,6 +857,19 @@
"dest": "cargo/vendor/cpufeatures-0.2.17",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/cpufeatures/cpufeatures-0.3.1.crate",
"sha256": "5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566",
"dest": "cargo/vendor/cpufeatures-0.3.1"
},
{
"type": "inline",
"contents": "{\"package\": \"5ca28b0ae3115b884660db4118d803791fd6756b6e88f39c0f3f7859060d7566\", \"files\": {}}",
"dest": "cargo/vendor/cpufeatures-0.3.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -779,6 +922,19 @@
"dest": "cargo/vendor/crypto-common-0.1.7",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/crypto-common/crypto-common-0.2.2.crate",
"sha256": "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453",
"dest": "cargo/vendor/crypto-common-0.2.2"
},
{
"type": "inline",
"contents": "{\"package\": \"ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453\", \"files\": {}}",
"dest": "cargo/vendor/crypto-common-0.2.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -844,6 +1000,19 @@
"dest": "cargo/vendor/digest-0.10.7",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/digest/digest-0.11.3.crate",
"sha256": "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2",
"dest": "cargo/vendor/digest-0.11.3"
},
{
"type": "inline",
"contents": "{\"package\": \"f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2\", \"files\": {}}",
"dest": "cargo/vendor/digest-0.11.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -922,6 +1091,19 @@
"dest": "cargo/vendor/downcast-rs-1.2.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/ecb/ecb-0.2.1.crate",
"sha256": "26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac",
"dest": "cargo/vendor/ecb-0.2.1"
},
{
"type": "inline",
"contents": "{\"package\": \"26f2a8b3e564eba0877223dc343703ad0385794e882e6d13f3a4dd5c6b1f41ac\", \"files\": {}}",
"dest": "cargo/vendor/ecb-0.2.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -1013,6 +1195,19 @@
"dest": "cargo/vendor/enum_dispatch-0.3.13",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/env_logger/env_logger-0.10.2.crate",
"sha256": "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580",
"dest": "cargo/vendor/env_logger-0.10.2"
},
{
"type": "inline",
"contents": "{\"package\": \"4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580\", \"files\": {}}",
"dest": "cargo/vendor/env_logger-0.10.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -1754,6 +1949,19 @@
"dest": "cargo/vendor/home-0.5.12",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/hybrid-array/hybrid-array-0.4.15.crate",
"sha256": "27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17",
"dest": "cargo/vendor/hybrid-array-0.4.15"
},
{
"type": "inline",
"contents": "{\"package\": \"27f864f10dfb56725ce5ce5472bc52252c8f93a4ab86327122cebf62c5f59a17\", \"files\": {}}",
"dest": "cargo/vendor/hybrid-array-0.4.15",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -1936,6 +2144,19 @@
"dest": "cargo/vendor/indexmap-2.14.2",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/inout/inout-0.2.5.crate",
"sha256": "3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f",
"dest": "cargo/vendor/inout-0.2.5"
},
{
"type": "inline",
"contents": "{\"package\": \"3b5cf4372d022092674a83b8febf5865f7a573cc0c5baec5e49c879b6a20978f\", \"files\": {}}",
"dest": "cargo/vendor/inout-0.2.5",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -2222,6 +2443,19 @@
"dest": "cargo/vendor/log-0.4.34",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/lopdf/lopdf-0.46.0.crate",
"sha256": "d43f64cb8c0d673ff84245a6de9e0172c0cc05dc5dd9d263660b5b4a02b6c4ae",
"dest": "cargo/vendor/lopdf-0.46.0"
},
{
"type": "inline",
"contents": "{\"package\": \"d43f64cb8c0d673ff84245a6de9e0172c0cc05dc5dd9d263660b5b4a02b6c4ae\", \"files\": {}}",
"dest": "cargo/vendor/lopdf-0.46.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -2248,6 +2482,19 @@
"dest": "cargo/vendor/malloc_buf-0.0.6",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/md-5/md-5-0.11.0.crate",
"sha256": "69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98",
"dest": "cargo/vendor/md-5-0.11.0"
},
{
"type": "inline",
"contents": "{\"package\": \"69b6441f590336821bb897fb28fc622898ccceb1d6cea3fde5ea86b090c4de98\", \"files\": {}}",
"dest": "cargo/vendor/md-5-0.11.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -2430,6 +2677,19 @@
"dest": "cargo/vendor/nohash-hasher-0.2.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/nom/nom-8.0.0.crate",
"sha256": "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405",
"dest": "cargo/vendor/nom-8.0.0"
},
{
"type": "inline",
"contents": "{\"package\": \"df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405\", \"files\": {}}",
"dest": "cargo/vendor/nom-8.0.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -3119,6 +3379,32 @@
"dest": "cargo/vendor/r-efi-6.0.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/rand/rand-0.10.3.crate",
"sha256": "65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af",
"dest": "cargo/vendor/rand-0.10.3"
},
{
"type": "inline",
"contents": "{\"package\": \"65c9fb96cbc91e3478eaae79a69fcd3f1ae4ad052e471fe6732fff548984b4af\", \"files\": {}}",
"dest": "cargo/vendor/rand-0.10.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/rand_core/rand_core-0.10.1.crate",
"sha256": "63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69",
"dest": "cargo/vendor/rand_core-0.10.1"
},
{
"type": "inline",
"contents": "{\"package\": \"63b8176103e19a2643978565ca18b50549f6101881c443590420e4dc998a3c69\", \"files\": {}}",
"dest": "cargo/vendor/rand_core-0.10.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -3132,6 +3418,19 @@
"dest": "cargo/vendor/range-alloc-0.1.5",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/rangemap/rangemap-1.8.0.crate",
"sha256": "a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d",
"dest": "cargo/vendor/rangemap-1.8.0"
},
{
"type": "inline",
"contents": "{\"package\": \"a611d15b50743feb4c76b7d03edcb0e64f399c26961e4efe6975bc398be6aa3d\", \"files\": {}}",
"dest": "cargo/vendor/rangemap-1.8.0",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -3535,6 +3834,19 @@
"dest": "cargo/vendor/sha2-0.10.9",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/sha2/sha2-0.11.1.crate",
"sha256": "47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19",
"dest": "cargo/vendor/sha2-0.11.1"
},
{
"type": "inline",
"contents": "{\"package\": \"47d7069beb7d6ac7b9acd1039986e73443f24234f41074da099d6f994ac9ad19\", \"files\": {}}",
"dest": "cargo/vendor/sha2-0.11.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -3743,6 +4055,19 @@
"dest": "cargo/vendor/strict-num-0.1.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/stringprep/stringprep-0.1.5.crate",
"sha256": "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1",
"dest": "cargo/vendor/stringprep-0.1.5"
},
{
"type": "inline",
"contents": "{\"package\": \"7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1\", \"files\": {}}",
"dest": "cargo/vendor/stringprep-0.1.5",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -4042,6 +4367,19 @@
"dest": "cargo/vendor/tinystr-0.8.4",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/tinyvec/tinyvec-1.13.3.crate",
"sha256": "fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee",
"dest": "cargo/vendor/tinyvec-1.13.3"
},
{
"type": "inline",
"contents": "{\"package\": \"fd3ca314f692efd6c868f8408f53fe444634a845f96c028b97d35f6a1f79f0ee\", \"files\": {}}",
"dest": "cargo/vendor/tinyvec-1.13.3",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -4146,6 +4484,19 @@
"dest": "cargo/vendor/typenum-1.20.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/unicode-bidi/unicode-bidi-0.3.18.crate",
"sha256": "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5",
"dest": "cargo/vendor/unicode-bidi-0.3.18"
},
{
"type": "inline",
"contents": "{\"package\": \"5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5\", \"files\": {}}",
"dest": "cargo/vendor/unicode-bidi-0.3.18",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -4159,6 +4510,32 @@
"dest": "cargo/vendor/unicode-ident-1.0.26",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/unicode-normalization/unicode-normalization-0.1.26.crate",
"sha256": "75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d",
"dest": "cargo/vendor/unicode-normalization-0.1.26"
},
{
"type": "inline",
"contents": "{\"package\": \"75cd3fb03d5288b3ba9a1ee103ac7e9cbb59f0b03d64f8ddfcc04dd42758a85d\", \"files\": {}}",
"dest": "cargo/vendor/unicode-normalization-0.1.26",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/unicode-properties/unicode-properties-0.1.4.crate",
"sha256": "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d",
"dest": "cargo/vendor/unicode-properties-0.1.4"
},
{
"type": "inline",
"contents": "{\"package\": \"7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d\", \"files\": {}}",
"dest": "cargo/vendor/unicode-properties-0.1.4",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
@@ -4601,6 +4978,19 @@
"dest": "cargo/vendor/weezl-0.1.12",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
"url": "https://static.crates.io/crates/weezl/weezl-0.2.1.crate",
"sha256": "d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2",
"dest": "cargo/vendor/weezl-0.2.1"
},
{
"type": "inline",
"contents": "{\"package\": \"d4ca08e5ef825b65b056d9efbd95c8750683f0a6d0466d02e96dc2e4e360f3d2\", \"files\": {}}",
"dest": "cargo/vendor/weezl-0.2.1",
"dest-filename": ".cargo-checksum.json"
},
{
"type": "archive",
"archive-type": "tar-gzip",
+15
View File
@@ -84,6 +84,21 @@
</branding>
<content_rating type="oars-1.1" />
<releases>
<release version="0.39.2" date="2026-10-27">
<description>
<p>PDF export now uses its own small PDF writer on lopdf 0.46 instead of printpdf; the exported files look the same.</p>
</description>
</release>
<release version="0.39.1" date="2026-10-27">
<description>
<p>Windows: the mouse works again; Windows Ink no longer reports it as a pen.</p>
</description>
</release>
<release version="0.39.0" date="2026-10-27">
<description>
<p>First experimental Windows version as a portable program. Internal groundwork for Android; nothing changes on Linux.</p>
</description>
</release>
<release version="0.38.0" date="2026-10-27">
<description>
<p>The notebooks can be stored in a folder synced by Nextcloud, Google Drive, Syncthing or Dropbox. Changes from other devices are taken over automatically, conflict copies become pages of their own, and a warning appears when a notebook is open on another device.</p>
+3
View File
@@ -3,6 +3,9 @@
Notebook app for pen, touch and mouse on Linux (tested on Zorin OS 18 / Ubuntu 24.04).
### Highlights:
- **PDF export on current lopdf**: The PDF export uses its own small PDF writer on top of lopdf 0.46 instead of the outdated printpdf 0.7 (which pinned lopdf 0.31). Exported PDFs look the same – layers, searchable text, images, highlighters and bookmarks stay as before
- **Windows mouse fix**: The mouse works normally again in the Windows version – Windows Ink reported it as a pen, so clicks and moves went the wrong way. Nothing changes on Linux
- **Windows (experimental)**: `ink-notes_<version>_x86_64.exe` is a portable program for Windows 10/11 – just start it, no installation. Notebooks and settings are stored in `%APPDATA%\ink-notes`, pen input uses Windows Ink, updates replace the `.exe` and take effect at the next start. Not yet tested on real tablet hardware – feedback welcome. Nothing changes on Linux; internally the groundwork for an Android version was laid
- **Sync between devices**: ☰ → Storage location puts the notebooks into a folder synced by Nextcloud, Google Drive, Syncthing or Dropbox (existing notebooks are copied, the originals stay). Changes from other devices show up within seconds while everything local is saved, conflict copies of the sync client become pages of their own, and opening a notebook that is open on another device shows a warning
- **Import from Saber**: ☰ → Import (or dragging the file into the window) now also opens Saber notebooks (`.sbn2` with its asset files next to it, `.sba` archives, old `.sbn`) – strokes with pen pressure, highlighters, shapes, typed text, images and PDF backgrounds
- **Import from Xournal++ and Rnote**: ☰ → Import (or dragging the file into the window) now also opens Xournal++ (`.xopp`, `.xoj`) and Rnote (`.rnote`) notebooks as new notebooks – strokes with pen pressure, highlighters, shapes, texts, images, paper patterns and Xournal++ PDF backgrounds; PDF pages imported in Rnote become images, infinite Rnote documents an endless notebook
+9 -5
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# ==============================================================================
# Haupt-Release-Build-Skript für ink-notes (Zorin OS 18 / Linux)
# Erstellt .deb, .AppImage, SHA256-Prüfsummen und deren Minisign-Signatur in target/
# Erstellt .deb, .AppImage, Windows-.exe, SHA256-Prüfsummen und deren Minisign-Signatur in target/
# ==============================================================================
set -euo pipefail
@@ -13,6 +13,7 @@ cd "${ROOT_DIR}"
VERSION=$(grep -m1 '^version = ' Cargo.toml | cut -d '"' -f2)
DEB_NAME="ink-notes_${VERSION}_amd64.deb"
APPIMAGE_NAME="ink-notes_${VERSION}_x86_64.AppImage"
EXE_NAME="ink-notes_${VERSION}_x86_64.exe"
CHECKSUM_FILE="SHA256SUMS.txt"
echo "======================================================================"
@@ -25,12 +26,15 @@ echo "======================================================================"
# 2. AppImage bauen (.AppImage)
"${SCRIPT_DIR}/package-appimage.sh"
# 3. Checksums berechnen
# 3. Portable Windows program (.exe)
"${SCRIPT_DIR}/package-windows.sh"
# 4. Checksums berechnen
echo "=== Berechne SHA256-Prüfsummen in target/${CHECKSUM_FILE} ==="
cd "${ROOT_DIR}/target"
sha256sum "${DEB_NAME}" "${APPIMAGE_NAME}" > "${CHECKSUM_FILE}"
sha256sum "${DEB_NAME}" "${APPIMAGE_NAME}" "${EXE_NAME}" > "${CHECKSUM_FILE}"
# 4. Prüfsummen signieren (Pflicht für In-App-Updates)
# 5. Prüfsummen signieren (Pflicht für In-App-Updates)
# Signiert wird mit rsign2 (minisign-kompatibles Format): Der Schlüssel wurde mit
# `rsign generate -W` erzeugt; das minisign-CLI kann ihn ohne Terminal nicht laden.
SIGNATURE_FILE="${CHECKSUM_FILE}.minisig"
@@ -73,7 +77,7 @@ fi
echo "======================================================================"
echo "Release-Artefakte für v${VERSION} erfolgreich erstellt:"
echo "======================================================================"
ls -lh "${DEB_NAME}" "${APPIMAGE_NAME}" "${CHECKSUM_FILE}"
ls -lh "${DEB_NAME}" "${APPIMAGE_NAME}" "${EXE_NAME}" "${CHECKSUM_FILE}"
if [ -f "${SIGNATURE_FILE}" ]; then ls -lh "${SIGNATURE_FILE}"; fi
echo ""
echo "Inhalt von target/${CHECKSUM_FILE}:"
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env bash
# ==============================================================================
# Builds the portable Windows program target/ink-notes_<version>_x86_64.exe (Issue #48)
# by cross-compiling on Linux. Needs the MinGW-w64 compiler
# (Debian/Ubuntu: gcc-mingw-w64-x86-64) for the C parts of the dependencies.
# ==============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${ROOT_DIR}"
TARGET="x86_64-pc-windows-gnu"
VERSION=$(grep -m1 '^version = ' Cargo.toml | cut -d '"' -f2)
EXE_NAME="ink-notes_${VERSION}_x86_64.exe"
if ! command -v x86_64-w64-mingw32-gcc >/dev/null 2>&1; then
echo "FEHLER: x86_64-w64-mingw32-gcc nicht gefunden (Paket gcc-mingw-w64-x86-64)."
exit 1
fi
if command -v rustup >/dev/null 2>&1; then
rustup target add "${TARGET}"
fi
echo "=== Baue ${EXE_NAME} ==="
cargo build --release --target "${TARGET}"
cp "${CARGO_TARGET_DIR:-target}/${TARGET}/release/ink-notes.exe" "target/${EXE_NAME}"
ls -lh "target/${EXE_NAME}"
+4 -2
View File
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
# ==============================================================================
# Veröffentlicht ein Release auf dem Gitea-Server (gitea.pansi.eu)
# Lädt .deb, .AppImage, SHA256SUMS.txt und SHA256SUMS.txt.minisig idempotent als Release-Assets hoch.
# Lädt .deb, .AppImage, .exe, SHA256SUMS.txt und SHA256SUMS.txt.minisig idempotent als Release-Assets hoch.
# ==============================================================================
set -euo pipefail
@@ -32,6 +32,7 @@ RELEASE_TITLE="ink-notes ${TAG_NAME}"
DEB_FILE="${ROOT_DIR}/target/ink-notes_${VERSION}_amd64.deb"
APPIMAGE_FILE="${ROOT_DIR}/target/ink-notes_${VERSION}_x86_64.AppImage"
EXE_FILE="${ROOT_DIR}/target/ink-notes_${VERSION}_x86_64.exe"
CHECKSUMS_FILE="${ROOT_DIR}/target/SHA256SUMS.txt"
SIGNATURE_FILE="${CHECKSUMS_FILE}.minisig"
TEMPLATE_FILE="${ROOT_DIR}/packaging/release-notes.md.tmpl"
@@ -42,7 +43,7 @@ if [ ! -f "${TEMPLATE_FILE}" ]; then
fi
# Sicherstellen, dass die Artefakte existieren
if [ ! -f "${DEB_FILE}" ] || [ ! -f "${APPIMAGE_FILE}" ] || [ ! -f "${CHECKSUMS_FILE}" ]; then
if [ ! -f "${DEB_FILE}" ] || [ ! -f "${APPIMAGE_FILE}" ] || [ ! -f "${EXE_FILE}" ] || [ ! -f "${CHECKSUMS_FILE}" ]; then
echo "Artefakte nicht vollständig gefunden. Führe build-release.sh aus..."
"${SCRIPT_DIR}/build-release.sh"
fi
@@ -236,6 +237,7 @@ except Exception:
upload_asset "${DEB_FILE}"
upload_asset "${APPIMAGE_FILE}"
upload_asset "${EXE_FILE}"
upload_asset "${CHECKSUMS_FILE}"
upload_asset "${SIGNATURE_FILE}"
+61 -43
View File
@@ -3,6 +3,18 @@
//! notebook persistence (autosave). Lives in the library so that other entry points (e.g.
//! Android, Issue #42) can start it too.
// On Android the pen state is unused until pen input arrives there (Issue #44)
#![cfg_attr(
target_os = "android",
allow(
unused_mut,
unused_variables,
unused_assignments,
unused_imports,
dead_code
)
)]
use std::path::{Path, PathBuf};
use std::sync::mpsc::{self, Sender};
use std::sync::Arc;
@@ -11,10 +23,8 @@ use winit::{
event::{
ElementState, Event, KeyEvent, MouseButton, MouseScrollDelta, TouchPhase, WindowEvent,
},
event_loop::{ControlFlow, EventLoopBuilder},
event_loop::ControlFlow,
keyboard::{Key, ModifiersState, NamedKey},
platform::{wayland::EventLoopBuilderExtWayland, x11::EventLoopBuilderExtX11},
raw_window_handle::{HasDisplayHandle, RawDisplayHandle},
window::{Window, WindowBuilder},
};
@@ -31,7 +41,7 @@ use crate::image_layer::TextureSource;
use crate::library::Library;
use crate::osk::{self, KeyboardRequester};
use crate::pdf_doc::PdfRenderService;
use crate::platform::{self, DisplayBackend};
use crate::platform;
use crate::selection::{
render_selection_overlay, scale_factors, DragMode, SelectionHandleHit, SelectionState,
};
@@ -163,6 +173,7 @@ fn activate_tool(tool: ActiveTool, ui_state: &mut UiState, canvas: &mut CanvasSt
/// Kennung einer Stifttaste, die über Programmstarts hinweg gleich bleibt (die Tablet-
/// Schnittstelle liefert nur eine undurchsichtige, aber vergleichbare Kennung).
#[cfg(not(target_os = "android"))]
fn stylus_button_key(id: &octotablet::tool::ButtonID) -> u64 {
use std::hash::{Hash, Hasher};
// DefaultHasher::new() verwendet feste Schlüssel: gleiche Taste, gleicher Wert
@@ -523,7 +534,7 @@ impl Drop for SlowEventLog {
/// Zwischenablage: interne Objekte (Strg+C in ink-notes), Bilder aus der
/// Systemzwischenablage (Screenshots, aus anderen Programmen kopierte Bilder) und Text.
struct ClipboardBridge {
system: Option<arboard::Clipboard>,
system: Option<platform::clipboard::Clipboard>,
/// Prüfsumme des Systembildes zum Zeitpunkt des letzten internen Kopierens: Strg+V fügt
/// das Systembild nur ein, wenn es danach kopiert wurde (die jüngste Kopie gewinnt)
image_hash_at_copy: Option<[u8; 32]>,
@@ -544,9 +555,9 @@ impl ClipboardBridge {
}
/// Bild in der Systemzwischenablage samt Prüfsumme.
fn system_image(&mut self) -> Option<(arboard::ImageData<'static>, [u8; 32])> {
fn system_image(&mut self) -> Option<(platform::clipboard::ImageData<'static>, [u8; 32])> {
if self.system.is_none() {
self.system = arboard::Clipboard::new()
self.system = platform::clipboard::Clipboard::new()
.map_err(|e| eprintln!("[Zwischenablage] Nicht verfügbar: {e}"))
.ok();
}
@@ -563,7 +574,7 @@ impl ClipboardBridge {
/// Text in der Systemzwischenablage (nicht nur Leerraum).
fn system_text(&mut self) -> Option<String> {
if self.system.is_none() {
self.system = arboard::Clipboard::new().ok();
self.system = platform::clipboard::Clipboard::new().ok();
}
let text = self.system.as_mut()?.get_text().ok()?;
(!text.trim().is_empty()).then_some(text)
@@ -596,12 +607,12 @@ impl ClipboardBridge {
}
};
if self.system.is_none() {
self.system = arboard::Clipboard::new().ok();
self.system = platform::clipboard::Clipboard::new().ok();
}
let Some(system) = self.system.as_mut() else {
return false;
};
let data = arboard::ImageData {
let data = platform::clipboard::ImageData {
width: image.width as usize,
height: image.height as usize,
bytes: std::borrow::Cow::Owned(image.pixels),
@@ -1252,7 +1263,16 @@ fn insert_image_file(
}
/// Runs the app until the window is closed (the desktop entry point is `src/main.rs`).
#[cfg(not(target_os = "android"))]
pub fn run() -> Result<(), Box<dyn std::error::Error>> {
// Anzeigesystem wählen: Unter WSL (WSLg) bricht Wayland ab, X11 (Xwayland) läuft stabil
run_with(platform::desktop_event_loop()?)
}
/// Runs the app on `event_loop` (desktop: [`run`], Android: `android_main` in `lib.rs`).
pub fn run_with(
event_loop: winit::event_loop::EventLoop<()>,
) -> Result<(), Box<dyn std::error::Error>> {
// Gespeicherte UI-Einstellungen; zuerst, damit alle Texte in der gewählten Sprache sind
let ui_settings_path = UiSettings::default_path();
let mut ui_settings = ui_settings_path
@@ -1261,20 +1281,6 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
.unwrap_or_default();
i18n::set_language(ui_settings.language.unwrap_or_else(Language::system));
// Anzeigesystem wählen: Unter WSL (WSLg) bricht Wayland ab, X11 (Xwayland) läuft stabil
let mut event_loop_builder = EventLoopBuilder::new();
match platform::display_backend_from_env() {
DisplayBackend::X11 => {
eprintln!("[Anzeige] Verwende X11 (unter WSL Standard; übersteuerbar mit INK_NOTES_DISPLAY=wayland)");
event_loop_builder.with_x11();
}
DisplayBackend::Wayland => {
eprintln!("[Anzeige] Verwende Wayland (INK_NOTES_DISPLAY=wayland)");
event_loop_builder.with_wayland();
}
DisplayBackend::Automatic => {}
}
let event_loop = event_loop_builder.build()?;
// App-Kennung wie die .desktop-Datei (eu.pansi.ink-notes): Daran ordnen Dock und Taskleiste
// das Fenster dem App-Symbol zu (Wayland: app_id, X11: WM_CLASS). Das Fenstersymbol selbst
// nutzt nur X11.
@@ -1282,10 +1288,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
.with_title("ink-notes")
.with_inner_size(winit::dpi::LogicalSize::new(1280.0, 850.0))
.with_window_icon(app_icon());
let builder =
winit::platform::wayland::WindowBuilderExtWayland::with_name(builder, APP_ID, "ink-notes");
let builder =
winit::platform::x11::WindowBuilderExtX11::with_name(builder, APP_ID, "ink-notes");
let builder = platform::with_app_id(builder, APP_ID);
let window = Arc::new(builder.build(&event_loop)?);
let mut canvas = match pollster::block_on(CanvasState::new(window.clone())) {
@@ -1296,25 +1299,36 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
}
};
let mut tablet_manager = match octotablet::builder::Builder::default().build_shared(&window) {
Ok(mgr) => {
// Android has no octotablet: the pen arrives as touch events there (Issue #44).
// octotablet panics on Windows without the Tablet PC components (RealTimeStylus not
// registered); the app then runs with mouse and touch events only
#[cfg(not(target_os = "android"))]
let tablet_result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
// Windows Ink would otherwise report the mouse as a pen (Issue #48): every mouse
// move counted as pen hover, every click as pen down next to the normal mouse events
octotablet::builder::Builder::default()
.emulate_tool_from_mouse(false)
.build_shared(&window)
}));
#[cfg(not(target_os = "android"))]
let mut tablet_manager = match tablet_result {
Ok(Ok(mgr)) => {
println!("[Input] Octotablet Stylus-/Tablet-Manager erfolgreich initialisiert (Wayland/X11 Tablet-Protokoll aktiv).");
Some(mgr)
}
Err(err) => {
Ok(Err(err)) => {
println!("[Input] Hinweis: Octotablet Tablet-Manager nicht aktiv ({err}). Nutze Standard-Event-Pipeline.");
None
}
Err(_) => {
println!("[Input] Hinweis: Octotablet Tablet-Manager abgestürzt. Nutze Standard-Event-Pipeline.");
None
}
};
// Bildschirmtastatur (nur Wayland): eigenes text-input-Objekt, siehe osk.rs
let mut keyboard_requester = match window.display_handle().map(|handle| handle.as_raw()) {
// Safety: `window` lebt bis zum Ende von main, die Event-Schleife besitzt beide
Ok(RawDisplayHandle::Wayland(handle)) => {
Some(unsafe { KeyboardRequester::new(handle.display.as_ptr()) })
}
_ => None,
};
// Safety: `window` lives until the end of run(), the event loop owns both
let mut keyboard_requester = unsafe { KeyboardRequester::for_window(&window) };
let mut ime_was_active = false;
let mut ime_composing = false;
@@ -1515,6 +1529,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
// that touch it while the pen is near, are ignored until they are lifted
let mut palm_touch_ids: std::collections::HashSet<u64> = std::collections::HashSet::new();
let mut stylus_left_at: Option<Instant> = None;
#[cfg(not(target_os = "android"))]
let mut stylus_tool_id: Option<octotablet::tool::ID> = None;
// Gedrückte Stifttaste (Kennung, Belegung) und das Werkzeug, das während eines Strichs mit
// einer Werkzeug-Taste (Auswählen, Textmarker, Stift) vertreten wird
@@ -1695,7 +1710,10 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
}
// Stylus-/Tablet-Events via Octotablet abfragen (Wayland/X11 Tablet-Protokoll für aktive Stifte wie Lazarite)
#[cfg(not(target_os = "android"))]
if let Some(ref mut mgr) = tablet_manager {
// Infallible on Windows
#[allow(irrefutable_let_patterns)]
if let Ok(tablet_events) = mgr.pump() {
let scale = window.scale_factor() as f32;
let now = start_time.elapsed().as_secs_f64();
@@ -1744,8 +1762,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
pinch_prev_distance = None;
pinch_prev_center = None;
stylus_left_at = None;
#[cfg(target_os = "linux")]
winit::platform::wayland::ignore_decoration_touches_until(Some(
platform::ignore_decoration_touches_until(Some(
Instant::now() + Duration::from_secs(24 * 3600),
));
window.request_redraw();
@@ -1760,8 +1777,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
| octotablet::events::ToolEvent::Removed => {
if is_stylus_in_proximity {
stylus_left_at = Some(Instant::now());
#[cfg(target_os = "linux")]
winit::platform::wayland::ignore_decoration_touches_until(Some(
platform::ignore_decoration_touches_until(Some(
Instant::now() + PALM_GRACE,
));
}
@@ -3038,7 +3054,7 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
let handled_at = Instant::now();
let event_time = match touch.phase {
TouchPhase::Cancelled => None,
phase => winit::platform::wayland::take_touch_time(touch.id, phase),
phase => platform::take_touch_time(touch.id, phase),
};
if let (TouchPhase::Started, Some(time)) = (touch.phase, event_time) {
touch_clock = Some((handled_at, time));
@@ -4139,6 +4155,8 @@ pub fn run() -> Result<(), Box<dyn std::error::Error>> {
UiAction::InstallOrOpenUpdate(path) => {
if let Err(err) = updater.install_or_open(&path) {
ui_state.set_export_message(t!("Update-Fehler: {err}", err = err));
} else if matches!(updater.runtime(), crate::updater::RuntimeEnvironment::WindowsExe { .. }) {
ui_state.set_export_message(t!("Update installiert – gilt ab dem nächsten Start"));
} else {
ui_state.set_export_message(t!("Update gestartet / geöffnet"));
}
+3 -15
View File
@@ -690,21 +690,9 @@ impl AutosaveController {
/// Liefert das Verzeichnis für Notfall-Wiederherstellungsdateien ($XDG_DATA_HOME/ink-notes/recovery).
pub fn recovery_dir() -> PathBuf {
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
if !xdg.is_empty() {
return PathBuf::from(xdg).join("ink-notes").join("recovery");
}
}
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
return PathBuf::from(home)
.join(".local")
.join("share")
.join("ink-notes")
.join("recovery");
}
}
PathBuf::from(".").join(".recovery")
crate::paths::data_dir()
.map(|d| d.join("recovery"))
.unwrap_or_else(|| PathBuf::from(".").join(".recovery"))
}
impl Drop for AutosaveController {
+67 -127
View File
@@ -1,4 +1,4 @@
//! Verlustfreier Vektor-PDF-Export des gesamten Notizbuchs via printpdf.
//! Verlustfreier Vektor-PDF-Export des gesamten Notizbuchs (PDF-Ausgabe: [`crate::pdf_writer`]).
//!
//! Features:
//! - Hochformat DIN A4 (210 mm x 297 mm)
@@ -8,7 +8,7 @@
//! - Vektor-Hintergründe (Kariert 5mm, Liniert, Punktraster, Blanko)
//! - Zwei Export-Strategien:
//! - `FilledPolygon`: Glatte gefüllte Polygone aus den Triangle-Strip-Hüllen (variable Pinselstärke)
//! - `BezierCurve`: Glatte kubische Bézier-Kurven via `printpdf::Line`
//! - `BezierCurve`: Glatte kubische Bézier-Kurven via `pdf_writer::Line`
use std::collections::HashMap;
use std::error::Error;
@@ -16,14 +16,15 @@ use std::fs::File;
use std::io::BufWriter;
use std::path::{Path, PathBuf};
use printpdf::path::{PaintMode, WindingOrder};
use printpdf::{Color, Line, Mm, PdfDocument, PdfDocumentReference, Point, Polygon, Rgb};
use crate::assets::AssetStore;
use crate::background::{PatternColor, DOT_SIZE, GRID_5MM_SPACING, LINED_SPACING};
use crate::document::{
Notebook, Page, PageBackground, Stroke, A4_HEIGHT_MM, A4_WIDTH_MM, PAGE_HEIGHT, PAGE_WIDTH,
};
use crate::pdf_writer::{
Color, Line, Mm, PaintMode, PdfDocument, PdfDocumentReference, Point, Polygon, Rgb,
WindingOrder,
};
use crate::t;
use ab_glyph::Font as _;
@@ -56,9 +57,7 @@ pub fn default_documents_dir() -> PathBuf {
}
}
if let Ok(home) = std::env::var("HOME") {
if !home.trim().is_empty() {
let home = PathBuf::from(home);
if let Some(home) = crate::paths::home_dir() {
let user_dirs =
std::fs::read_to_string(home.join(".config/user-dirs.dirs")).unwrap_or_default();
if let Some(dir) =
@@ -69,7 +68,6 @@ pub fn default_documents_dir() -> PathBuf {
}
return home.join("Documents");
}
}
PathBuf::from(".")
}
@@ -150,19 +148,19 @@ fn pdf_page_mm(page: &Page) -> (Mm, Mm) {
/// Beginnt eine Ebene: Koordinaten beziehen sich auf eine A4-hohe Seite (siehe
/// [`page_to_pdf_mm`]) und werden auf die tatsächliche Höhe von `page` verschoben.
fn begin_page_layer(layer: &printpdf::PdfLayerReference, page: &Page) {
fn begin_page_layer(layer: &crate::pdf_writer::PdfLayerReference, page: &Page) {
layer.save_graphics_state();
let dy = (page.size.y - PAGE_HEIGHT) * PT_PER_UNIT;
if dy != 0.0 {
layer.set_ctm(printpdf::CurTransMat::Translate(
printpdf::Pt(0.0),
printpdf::Pt(dy),
layer.set_ctm(crate::pdf_writer::CurTransMat::Translate(
crate::pdf_writer::Pt(0.0),
crate::pdf_writer::Pt(dy),
));
}
}
/// Beendet eine mit [`begin_page_layer`] begonnene Ebene.
fn end_page_layer(layer: &printpdf::PdfLayerReference) {
fn end_page_layer(layer: &crate::pdf_writer::PdfLayerReference) {
layer.restore_graphics_state();
}
@@ -214,7 +212,7 @@ pub fn export_notebook_to_bytes(
return export_notebook_to_bytes(&cropped, strategy, assets, pattern);
}
let doc = generate_pdf_document(notebook, strategy, assets, pattern)?;
let bytes = add_alpha_states(fix_text_encoding(doc.save_to_bytes()?, notebook)?, notebook)?;
let bytes = add_alpha_states(doc.save_to_bytes()?, notebook)?;
// Importierte PDF-Seiten als Vektorgrafik unterlegen
let underlays = pdf_underlays(notebook);
@@ -240,7 +238,7 @@ pub fn export_notebook_to_bytes(
raster.insert(underlay.target_page);
}
let doc = generate_pdf_document_with(notebook, strategy, assets, pattern, &raster)?;
let bytes = add_alpha_states(fix_text_encoding(doc.save_to_bytes()?, notebook)?, notebook)?;
let bytes = add_alpha_states(doc.save_to_bytes()?, notebook)?;
let remaining: Vec<_> = underlays
.into_iter()
.filter(|u| !raster.contains(&u.target_page))
@@ -252,35 +250,6 @@ pub fn export_notebook_to_bytes(
Ok(merged)
}
/// printpdf schreibt Lesezeichen-Titel als rohes UTF-8, PDF-Betrachter lesen Textstrings aber
/// als PDFDocEncoding oder UTF-16BE: Umlaute und Gedankenstriche würden verstümmelt. Titel
/// mit Nicht-ASCII-Zeichen werden deshalb nach UTF-16BE (mit BOM) umgeschrieben.
fn fix_text_encoding(bytes: Vec<u8>, notebook: &Notebook) -> Result<Vec<u8>, Box<dyn Error>> {
let needs_fix = notebook
.pages
.iter()
.filter_map(|p| p.title.as_deref())
.any(|t| !t.is_ascii());
if !needs_fix {
return Ok(bytes);
}
let mut doc = printpdf::lopdf::Document::load_mem(&bytes)?;
for object in doc.objects.values_mut() {
let Ok(dict) = object.as_dict_mut() else {
continue;
};
if let Ok(printpdf::lopdf::Object::String(raw, format)) = dict.get_mut(b"Title") {
if let Some(utf16) = utf16be_text(raw) {
*raw = utf16;
*format = printpdf::lopdf::StringFormat::Hexadecimal;
}
}
}
let mut out = Vec::new();
doc.save_to(&mut out)?;
Ok(out)
}
/// Name des Grafikzustands für halbtransparente Tinte mit Deckkraft `alpha` (auf Prozent
/// gerundet); `None` für deckende Farben.
fn alpha_state_name(alpha: f32) -> Option<String> {
@@ -288,12 +257,12 @@ fn alpha_state_name(alpha: f32) -> Option<String> {
(percent < 100).then(|| format!("InkAlpha{percent}"))
}
/// printpdf kann keine Grafikzustände mit Deckkraft anlegen: Die Tintenebene verweist mit
/// Grafikzustände mit Deckkraft: Die Tintenebene verweist mit
/// `gs` auf die Namen aus [`alpha_state_name`], die Zustände selbst (`ca`/`CA`) werden hier
/// in die Ressourcen jeder Seite eingetragen. Ohne halbtransparente Striche bleibt das PDF
/// unverändert.
fn add_alpha_states(bytes: Vec<u8>, notebook: &Notebook) -> Result<Vec<u8>, Box<dyn Error>> {
use printpdf::lopdf::{Dictionary, Object};
use lopdf::{Dictionary, Object};
let mut states: Vec<(String, f32)> = notebook
.pages
@@ -308,7 +277,7 @@ fn add_alpha_states(bytes: Vec<u8>, notebook: &Notebook) -> Result<Vec<u8>, Box<
states.sort_by(|a, b| a.0.cmp(&b.0));
states.dedup_by(|a, b| a.0 == b.0);
let mut doc = printpdf::lopdf::Document::load_mem(&bytes)?;
let mut doc = lopdf::Document::load_mem(&bytes)?;
let page_ids: Vec<_> = doc.get_pages().into_values().collect();
for page_id in page_ids {
let resources = dict_object_id(&mut doc, page_id, b"Resources")?;
@@ -331,11 +300,11 @@ fn add_alpha_states(bytes: Vec<u8>, notebook: &Notebook) -> Result<Vec<u8>, Box<
/// Kennung des Wörterbuchs unter `key` im Objekt `owner`; ein eingebettetes oder fehlendes
/// Wörterbuch wird dafür zu einem eigenen Objekt.
fn dict_object_id(
doc: &mut printpdf::lopdf::Document,
owner: printpdf::lopdf::ObjectId,
doc: &mut lopdf::Document,
owner: lopdf::ObjectId,
key: &[u8],
) -> Result<printpdf::lopdf::ObjectId, Box<dyn Error>> {
use printpdf::lopdf::{Dictionary, Object};
) -> Result<lopdf::ObjectId, Box<dyn Error>> {
use lopdf::{Dictionary, Object};
let entry = doc.get_object(owner)?.as_dict()?.get(key).ok().cloned();
if let Some(Object::Reference(id)) = entry {
@@ -352,20 +321,6 @@ fn dict_object_id(
Ok(id)
}
/// UTF-8-Text als PDF-Textstring in UTF-16BE mit BOM; `None` bei reinem ASCII oder
/// ungültigem UTF-8.
fn utf16be_text(raw: &[u8]) -> Option<Vec<u8>> {
let text = std::str::from_utf8(raw).ok()?;
if text.is_ascii() {
return None;
}
let mut out = vec![0xFE, 0xFF];
for unit in text.encode_utf16() {
out.extend_from_slice(&unit.to_be_bytes());
}
Some(out)
}
/// Unterlagen für alle Seiten mit importierter PDF-Seite.
fn pdf_underlays(notebook: &Notebook) -> Vec<crate::pdf_merge::Underlay> {
notebook
@@ -390,7 +345,7 @@ fn pdf_underlays(notebook: &Notebook) -> Vec<crate::pdf_merge::Underlay> {
.collect()
}
/// Generiert das `printpdf::PdfDocumentReference` für alle Seiten des Notizbuchs.
/// Generiert das `crate::pdf_writer::PdfDocumentReference` für alle Seiten des Notizbuchs.
/// Importierte PDF-Seiten sind darin noch nicht enthalten; die legt
/// [`export_notebook_to_bytes`] anschließend als Vektorgrafik darunter.
pub fn generate_pdf_document(
@@ -450,7 +405,7 @@ fn generate_pdf_document_with(
}
let page_ref = doc.get_page(page_idx);
render_background_layer(&page_ref.get_layer(layer_bg), page, pattern);
let layer = |name: &str, render: &dyn Fn(&printpdf::PdfLayerReference)| {
let layer = |name: &str, render: &dyn Fn(&crate::pdf_writer::PdfLayerReference)| {
let layer = page_ref.add_layer(name);
begin_page_layer(&layer, page);
render(&layer);
@@ -473,7 +428,7 @@ fn generate_pdf_document_with(
/// Rendert das Vektorraster auf Layer 1 ("Background").
fn render_background_layer(
layer: &printpdf::PdfLayerReference,
layer: &crate::pdf_writer::PdfLayerReference,
page: &Page,
pattern: PatternColor,
) {
@@ -484,7 +439,7 @@ fn render_background_layer(
.color(crate::background::PaperTheme::White)
.map(crate::background::linear_to_srgb);
let grid_color = Color::Rgb(Rgb::new(r, g, b, None));
layer.set_outline_color(grid_color.clone());
layer.set_outline_color(grid_color);
layer.set_outline_thickness(0.4); // Feine 0.15 mm Haarlinie (ca. 0.4 pt)
layer.set_fill_color(grid_color);
@@ -570,7 +525,11 @@ fn render_background_layer(
}
/// Rendert alle Striche einer Seite auf Layer 2 ("Ink").
fn render_ink_layer(layer: &printpdf::PdfLayerReference, page: &Page, strategy: ExportStrategy) {
fn render_ink_layer(
layer: &crate::pdf_writer::PdfLayerReference,
page: &Page,
strategy: ExportStrategy,
) {
for stroke in &page.strokes {
if stroke.is_object() {
continue;
@@ -581,9 +540,9 @@ fn render_ink_layer(layer: &printpdf::PdfLayerReference, page: &Page, strategy:
let alpha_state = alpha_state_name(stroke.color[3]);
if let Some(name) = &alpha_state {
layer.save_graphics_state();
layer.add_operation(printpdf::lopdf::content::Operation::new(
layer.add_operation(lopdf::content::Operation::new(
"gs",
vec![printpdf::lopdf::Object::Name(name.as_bytes().to_vec())],
vec![lopdf::Object::Name(name.as_bytes().to_vec())],
));
}
if let Some(ref shape) = stroke.shape {
@@ -651,8 +610,8 @@ fn pdf_image_xobject(
assets: &AssetStore,
asset: &str,
corners: &[glam::Vec2; 4],
) -> Result<printpdf::ImageXObject, Box<dyn Error>> {
use printpdf::{ColorBits, ColorSpace, ImageFilter, ImageXObject, Px};
) -> Result<crate::pdf_writer::ImageXObject, Box<dyn Error>> {
use crate::pdf_writer::{ColorSpace, ImageFilter, ImageXObject, Px};
let bytes = assets.read_bytes(asset)?;
let decoded = image::load_from_memory(&bytes)?;
@@ -668,12 +627,9 @@ fn pdf_image_xobject(
width: Px(decoded.width() as usize),
height: Px(decoded.height() as usize),
color_space,
bits_per_component: ColorBits::Bit8,
interpolate: true,
image_data: bytes,
image_filter: Some(ImageFilter::DCT),
smask: None,
clipping_bbox: None,
});
}
}
@@ -703,7 +659,7 @@ fn pdf_image_xobject(
/// Rendert die eingefügten Bilder einer Seite. Fehlt eine Bilddatei, erscheint an ihrer
/// Stelle eine graue Fläche, damit der Export nicht abbricht.
fn render_image_layer(
layer: &printpdf::PdfLayerReference,
layer: &crate::pdf_writer::PdfLayerReference,
page: &Page,
assets: Option<&AssetStore>,
) {
@@ -728,7 +684,7 @@ fn render_image_layer(
fn embed_text_fonts(
doc: &PdfDocumentReference,
notebook: &Notebook,
) -> Result<HashMap<TextFont, printpdf::IndirectFontRef>, Box<dyn Error>> {
) -> Result<HashMap<TextFont, crate::pdf_writer::IndirectFontRef>, Box<dyn Error>> {
let mut fonts = HashMap::new();
let used = notebook
.pages
@@ -747,9 +703,9 @@ fn embed_text_fonts(
/// Koordinatensystem (eine Einheit = eine Seiteneinheit, y nach oben) gesetzt, das eine
/// Transformationsmatrix auf seine Ecken abbildet – so stimmen Drehung und Skalierung.
fn render_text_layer(
layer: &printpdf::PdfLayerReference,
layer: &crate::pdf_writer::PdfLayerReference,
page: &Page,
fonts: &HashMap<TextFont, printpdf::IndirectFontRef>,
fonts: &HashMap<TextFont, crate::pdf_writer::IndirectFontRef>,
) {
for stroke in &page.strokes {
let (Some(text), Some(corners)) = (&stroke.text, stroke.object_corners()) else {
@@ -766,7 +722,7 @@ fn render_text_layer(
let o = page_to_pdf_pt(origin);
let matrix = [right.x * k, -right.y * k, -down.x * k, down.y * k, o.x, o.y];
layer.save_graphics_state();
layer.set_ctm(printpdf::CurTransMat::Raw(matrix));
layer.set_ctm(crate::pdf_writer::CurTransMat::Raw(matrix));
layer.set_fill_color(Color::Rgb(Rgb::new(
stroke.color[0],
stroke.color[1],
@@ -779,9 +735,9 @@ fn render_text_layer(
if line.text.trim().is_empty() {
continue;
}
layer.set_text_matrix(printpdf::TextMatrix::Translate(
printpdf::Pt(0.0),
printpdf::Pt(-layout.baseline(i)),
layer.set_text_matrix(crate::pdf_writer::TextMatrix::Translate(
crate::pdf_writer::Pt(0.0),
crate::pdf_writer::Pt(-layout.baseline(i)),
));
// Glyphen samt Unterschneidung wie auf dem Bildschirm (Abstände in 1/1000 Em)
let face = text.font.face();
@@ -799,45 +755,31 @@ fn render_text_layer(
}
/// Graue 1×1-Fläche für fehlende Bilder.
fn placeholder_xobject() -> printpdf::ImageXObject {
fn placeholder_xobject() -> crate::pdf_writer::ImageXObject {
rgb_xobject(1, 1, vec![200, 200, 200])
}
fn rgb_xobject(width: u32, height: u32, rgb: Vec<u8>) -> printpdf::ImageXObject {
printpdf::ImageXObject {
width: printpdf::Px(width as usize),
height: printpdf::Px(height as usize),
color_space: printpdf::ColorSpace::Rgb,
bits_per_component: printpdf::ColorBits::Bit8,
fn rgb_xobject(width: u32, height: u32, rgb: Vec<u8>) -> crate::pdf_writer::ImageXObject {
crate::pdf_writer::ImageXObject {
width: crate::pdf_writer::Px(width as usize),
height: crate::pdf_writer::Px(height as usize),
color_space: crate::pdf_writer::ColorSpace::Rgb,
interpolate: true,
image_data: rgb,
image_filter: None,
smask: None,
clipping_bbox: None,
}
}
/// Zeichnet ein Bild so, dass es die Ecken oben links, oben rechts, unten rechts,
/// unten links ausfüllt.
fn draw_image_xobject(
layer: &printpdf::PdfLayerReference,
xobject: printpdf::ImageXObject,
layer: &crate::pdf_writer::PdfLayerReference,
xobject: crate::pdf_writer::ImageXObject,
corners: &[glam::Vec2; 4],
) {
let (width, height) = (xobject.width.0 as f32, xobject.height.0 as f32);
layer.save_graphics_state();
layer.set_ctm(printpdf::CurTransMat::Raw(image_matrix(corners)));
// Bei 72 dpi ist ein Pixel ein Punkt; die Skalierung bildet das Bild auf das
// Einheitsquadrat ab, die Matrix oben auf die Bildecken
printpdf::Image::from(xobject).add_to_layer(
layer.clone(),
printpdf::ImageTransform {
dpi: Some(72.0),
scale_x: Some(1.0 / width),
scale_y: Some(1.0 / height),
..Default::default()
},
);
layer.set_ctm(crate::pdf_writer::CurTransMat::Raw(image_matrix(corners)));
layer.add_image(xobject);
layer.restore_graphics_state();
}
@@ -847,7 +789,7 @@ const PDF_RASTER_DPI: f32 = 200.0;
/// Bettet die importierte PDF-Seite als gerendertes Bild ein (Ausweichlösung).
fn render_pdf_raster(
layer: &printpdf::PdfLayerReference,
layer: &crate::pdf_writer::PdfLayerReference,
page: &Page,
assets: Option<&AssetStore>,
) {
@@ -885,19 +827,19 @@ fn render_pdf_raster(
/// Rendert eine mathematisch bereinigte Form (Gerade, Kreis, Rechteck, Koordinatensystem)
/// als verlustfreien Vektorpfad im PDF.
fn render_recognized_shape(
layer: &printpdf::PdfLayerReference,
layer: &crate::pdf_writer::PdfLayerReference,
shape: &crate::geometry::RecognizedShape,
color: [f32; 4],
) {
use crate::geometry::RecognizedShape;
let stroke_color = Color::Rgb(Rgb::new(color[0], color[1], color[2], None));
layer.set_outline_color(stroke_color.clone());
layer.set_outline_color(stroke_color);
layer.set_outline_thickness(1.5); // ca. 0.5 mm Standard-Liniendicke
// Wie auf dem Bildschirm: spitze Ecken und stumpfe Enden (der Grafikzustand kann noch
// runde Verbindungen von einem vorherigen Freihandstrich enthalten)
layer.set_line_cap_style(printpdf::LineCapStyle::Butt);
layer.set_line_join_style(printpdf::LineJoinStyle::Miter);
layer.set_line_cap_style(crate::pdf_writer::LineCapStyle::Butt);
layer.set_line_join_style(crate::pdf_writer::LineJoinStyle::Miter);
match shape {
RecognizedShape::Line { start, end } => {
@@ -1181,7 +1123,7 @@ fn render_recognized_shape(
}
/// Strategie A: Gefülltes Polygon aus den Triangle-Strip-Hüllpunkten.
fn render_stroke_polygon(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
fn render_stroke_polygon(layer: &crate::pdf_writer::PdfLayerReference, stroke: &Stroke) {
if stroke.vertices.is_empty() {
return;
}
@@ -1269,8 +1211,8 @@ fn exact_outlines() -> bool {
*EXACT.get_or_init(|| std::env::var_os("INK_NOTES_EXACT_OUTLINES").is_some_and(|v| v != "0"))
}
/// Strategie B: Kubische Bézier-Kurven aus den Abtastpunkten via printpdf::Line.
fn render_stroke_bezier(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
/// Strategie B: Kubische Bézier-Kurven aus den Abtastpunkten via crate::pdf_writer::Line.
fn render_stroke_bezier(layer: &crate::pdf_writer::PdfLayerReference, stroke: &Stroke) {
if stroke.points.is_empty() {
return;
}
@@ -1282,8 +1224,8 @@ fn render_stroke_bezier(layer: &printpdf::PdfLayerReference, stroke: &Stroke) {
None,
));
layer.set_outline_color(outline_color);
layer.set_line_cap_style(printpdf::LineCapStyle::Round);
layer.set_line_join_style(printpdf::LineJoinStyle::Round);
layer.set_line_cap_style(crate::pdf_writer::LineCapStyle::Round);
layer.set_line_join_style(crate::pdf_writer::LineJoinStyle::Round);
// Durchschnittliche Linienstärke aus dem Druck berechnen
let avg_pressure =
@@ -1756,10 +1698,10 @@ mod tests {
PatternColor::default(),
)
.unwrap();
let doc = printpdf::lopdf::Document::load_mem(&bytes).unwrap();
let doc = lopdf::Document::load_mem(&bytes).unwrap();
assert_eq!(doc.get_pages().len(), 2);
for page_id in doc.get_pages().into_values() {
let content = doc.get_page_content(page_id).unwrap();
let content = doc.get_page_content(page_id);
assert!(
String::from_utf8_lossy(&content).starts_with("q "),
"Originalseite fehlt als Unterlage"
@@ -1867,7 +1809,7 @@ mod tests {
PatternColor::default(),
)
.unwrap();
let doc = printpdf::lopdf::Document::load_mem(&bytes).unwrap();
let doc = lopdf::Document::load_mem(&bytes).unwrap();
let catalog = doc.catalog().unwrap();
assert!(catalog.has(b"Outlines"), "Lesezeichen fehlen");
@@ -1878,8 +1820,7 @@ mod tests {
.filter_map(|o| o.as_dict().ok())
.filter(|d| d.has(b"Parent"))
.filter_map(|d| match d.get(b"Title") {
Ok(printpdf::lopdf::Object::String(raw, _)) => {
Some(if raw.starts_with(&[0xFE, 0xFF]) {
Ok(lopdf::Object::String(raw, _)) => Some(if raw.starts_with(&[0xFE, 0xFF]) {
let units: Vec<u16> = raw[2..]
.chunks(2)
.map(|c| u16::from_be_bytes([c[0], c[1]]))
@@ -1887,8 +1828,7 @@ mod tests {
String::from_utf16(&units).unwrap()
} else {
String::from_utf8(raw.clone()).unwrap()
})
}
}),
_ => None,
})
.collect();
+26 -1
View File
@@ -140,7 +140,7 @@ pub fn list_dir(dir: &Path, kind: PickerKind) -> std::io::Result<Vec<PickerEntry
/// mit den Ordnern aus `~/.config/user-dirs.dirs` (z. B. „Bilder“ statt „Pictures“). Die
/// Bezeichnungen sind deutsch, angezeigt werden sie mit `i18n::tr`.
pub fn shortcuts() -> Vec<(&'static str, PathBuf)> {
let Some(home) = std::env::var_os("HOME").map(PathBuf::from) else {
let Some(home) = crate::paths::home_dir() else {
return Vec::new();
};
let user_dirs =
@@ -162,6 +162,30 @@ pub fn shortcuts() -> Vec<(&'static str, PathBuf)> {
list.push((tn!("🖥 Schreibtisch"), p));
}
list.push((tn!("🏠 Persönlicher Ordner"), home));
// Drives (Windows has no single root)
#[cfg(windows)]
{
const DRIVES: [&str; 24] = [
"💽 C:", "💽 D:", "💽 E:", "💽 F:", "💽 G:", "💽 H:", "💽 I:", "💽 J:", "💽 K:",
"💽 L:", "💽 M:", "💽 N:", "💽 O:", "💽 P:", "💽 Q:", "💽 R:", "💽 S:", "💽 T:",
"💽 U:", "💽 V:", "💽 W:", "💽 X:", "💽 Y:", "💽 Z:",
];
// Looked up once: a disconnected network drive can take seconds to answer
static FOUND: std::sync::OnceLock<Vec<(&str, PathBuf)>> = std::sync::OnceLock::new();
let found = FOUND.get_or_init(|| {
DRIVES
.iter()
.map(|label| {
(
*label,
PathBuf::from(format!("{}\\", &label[label.len() - 2..])),
)
})
.filter(|(_, root)| root.is_dir())
.collect()
});
list.extend(found.iter().cloned());
}
list
}
@@ -254,6 +278,7 @@ mod tests {
}
#[test]
#[cfg(unix)]
fn test_parse_user_dirs() {
let home = Path::new("/home/anna");
let content = "# Kommentar\nXDG_DESKTOP_DIR=\"$HOME/Schreibtisch\"\nXDG_PICTURES_DIR=\"$HOME/Bilder\"\nXDG_MUSIC_DIR=\"/daten/Musik\"\n";
+5
View File
@@ -484,4 +484,9 @@ pub const EN: &[(&str, &str)] = &[
("✔ Diesen Ordner wählen", "✔ Choose this folder"),
("📁 Ändern …", "📁 Change …"),
("📁 Speicherort wählen", "📁 Choose storage location"),
("Update installiert – gilt ab dem nächsten Start", "Update installed – takes effect at the next start"),
("🔄 Update installieren", "🔄 Install update"),
("Konnte Backup ({path}) nicht anlegen: {e}. Update abgebrochen, altes Programm unverändert.", "Could not create backup ({path}): {e}. Update cancelled, old program unchanged."),
("Konnte Programm nicht ersetzen: {e}. Altes Programm bleibt unverändert.", "Could not replace the program: {e}. The old program stays unchanged."),
("Ungültiger Programmpfad.", "Invalid program path."),
];
+35
View File
@@ -18,8 +18,10 @@ pub mod import;
pub mod library;
pub mod osk;
pub mod page_export;
pub mod paths;
pub mod pdf_doc;
pub mod pdf_merge;
pub mod pdf_writer;
pub mod platform;
pub mod rnote;
pub mod ruler;
@@ -33,3 +35,36 @@ pub mod ui;
pub mod updater;
pub mod viewport;
pub mod xopp;
/// Entry point on Android (Issue #42), called by the native activity.
///
/// Builds with `cargo rustc --lib --crate-type cdylib --target aarch64-linux-android`. Not
/// usable yet: the window surface still has to be created on `Resumed` and dropped on
/// `Suspended`.
#[cfg(target_os = "android")]
#[no_mangle]
fn android_main(app: winit::platform::android::activity::AndroidApp) {
use winit::platform::android::EventLoopBuilderExtAndroid;
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Info)
.with_tag("ink-notes"),
);
if let Some(dir) = app.internal_data_path() {
paths::set_app_dir(dir);
}
let event_loop = match winit::event_loop::EventLoopBuilder::new()
.with_android_app(app)
.build()
{
Ok(event_loop) => event_loop,
Err(err) => {
log::error!("event loop: {err}");
return;
}
};
if let Err(err) = app::run_with(event_loop) {
log::error!("{err}");
}
}
+8
View File
@@ -3,6 +3,14 @@
//!
//! Desktop entry point; the app itself is [`ink_notes::app`].
// No console window next to the app on Windows (debug builds keep it for the log)
#![cfg_attr(all(windows, not(debug_assertions)), windows_subsystem = "windows")]
#[cfg(not(target_os = "android"))]
fn main() -> Result<(), Box<dyn std::error::Error>> {
ink_notes::app::run()
}
/// On Android the app starts in `android_main` of the library.
#[cfg(target_os = "android")]
fn main() {}
+21 -142
View File
@@ -12,21 +12,29 @@
//! ohnehin nicht aus. Daher meldet das eigene Objekt einen Platzhalter als Umgebungstext und
//! übersetzt die Löschwünsche in Tastendrücke (Rücktaste/Entf) für egui.
use std::ffi::c_void;
#[cfg(target_os = "linux")]
mod wayland;
#[cfg(target_os = "linux")]
pub use wayland::KeyboardRequester;
use wayland_client::{
delegate_noop,
protocol::{wl_registry, wl_seat},
Connection, Dispatch, EventQueue, QueueHandle,
};
use wayland_protocols::wp::text_input::zv3::client::{
zwp_text_input_manager_v3::ZwpTextInputManagerV3,
zwp_text_input_v3::{self, ZwpTextInputV3},
};
/// Elsewhere the system shows the on-screen keyboard by itself; nothing to request.
#[cfg(not(target_os = "linux"))]
pub struct KeyboardRequester;
/// Platzhalter-Umgebungstext: ein Zeichen vor der Schreibmarke, damit die Rücktaste der
/// Bildschirmtastatur immer etwas zu löschen hat (ein Byte = ein Zeichen)
const SURROUNDING_PLACEHOLDER: &str = " ";
#[cfg(not(target_os = "linux"))]
impl KeyboardRequester {
/// # Safety
/// Nothing to uphold; `unsafe` like the Wayland version.
pub unsafe fn for_window(_window: &winit::window::Window) -> Option<Self> {
None
}
pub fn request(&mut self) {}
pub fn take_deletions(&mut self) -> Vec<egui::Event> {
Vec::new()
}
}
/// Wandelt Texteingaben ohne Vorschau (Preedit) in normale Texteingaben um.
///
@@ -74,135 +82,6 @@ pub fn should_request(was_active: bool, active: bool, pressed: bool) -> bool {
active && (!was_active || pressed)
}
#[derive(Default)]
struct State {
seat: Option<wl_seat::WlSeat>,
manager: Option<ZwpTextInputManagerV3>,
text_input: Option<ZwpTextInputV3>,
/// Von der Bildschirmtastatur gewünschtes Löschen (Zeichen vor/nach der Schreibmarke)
delete_back: u32,
delete_forward: u32,
}
/// Eigenes text-input-Objekt, mit dem ink-notes die Bildschirmtastatur anfordert
pub struct KeyboardRequester {
conn: Connection,
queue: EventQueue<State>,
state: State,
}
impl KeyboardRequester {
/// Hängt sich an die Wayland-Verbindung des Fensters.
///
/// # Safety
/// `wl_display` muss auf die `wl_display` des Fensters zeigen und gültig bleiben, solange
/// der `KeyboardRequester` lebt.
pub unsafe fn new(wl_display: *mut c_void) -> Self {
// Safety: an den Aufrufer weitergereicht
let backend =
unsafe { wayland_backend::client::Backend::from_foreign_display(wl_display.cast()) };
let conn = Connection::from_backend(backend);
let queue = conn.new_event_queue();
conn.display().get_registry(&queue.handle(), ());
let _ = conn.flush();
Self {
conn,
queue,
state: State::default(),
}
}
/// Verarbeitet eingetroffene Ereignisse; einmal pro Frame aufrufen.
pub fn pump(&mut self) {
let _ = self.queue.dispatch_pending(&mut self.state);
if self.state.text_input.is_none() {
if let (Some(manager), Some(seat)) = (&self.state.manager, &self.state.seat) {
self.state.text_input =
Some(manager.get_text_input(seat, &self.queue.handle(), ()));
}
}
}
/// Bittet den Compositor, die Bildschirmtastatur einzublenden. Muss nach dem `enable` von
/// winit kommen (also nach `handle_platform_output`).
pub fn request(&mut self) {
self.pump();
if let Some(text_input) = &self.state.text_input {
text_input.enable();
self.send_surrounding_placeholder();
}
}
/// Tastendrücke für das seit dem letzten Aufruf gewünschte Löschen (vor `begin_frame`
/// aufrufen und an die egui-Eingabe anhängen)
pub fn take_deletions(&mut self) -> Vec<egui::Event> {
self.pump();
// Den Platzhalter hier nicht neu melden: Die Tastatur löscht bei jeder neuen Meldung
// erneut, solange die Rücktaste gedrückt ist – ein kurzes Antippen löschte sonst doppelt.
let back = std::mem::take(&mut self.state.delete_back);
let forward = std::mem::take(&mut self.state.delete_forward);
deletion_key_events(back, forward)
}
fn send_surrounding_placeholder(&self) {
if let Some(text_input) = &self.state.text_input {
let len = SURROUNDING_PLACEHOLDER.len() as i32;
text_input.set_surrounding_text(SURROUNDING_PLACEHOLDER.to_owned(), len, len);
text_input.commit();
let _ = self.conn.flush();
}
}
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global {
name, interface, ..
} = event
{
match interface.as_str() {
"wl_seat" if state.seat.is_none() => {
state.seat = Some(registry.bind(name, 1, qh, ()));
}
"zwp_text_input_manager_v3" if state.manager.is_none() => {
state.manager = Some(registry.bind(name, 1, qh, ()));
}
_ => {}
}
}
}
}
delegate_noop!(State: ignore wl_seat::WlSeat);
delegate_noop!(State: ZwpTextInputManagerV3);
impl Dispatch<ZwpTextInputV3, ()> for State {
fn event(
state: &mut Self,
_: &ZwpTextInputV3,
event: zwp_text_input_v3::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
// Getippter Text kommt über winit; hier nur das Löschen
if let zwp_text_input_v3::Event::DeleteSurroundingText {
before_length,
after_length,
} = event
{
state.delete_back += before_length;
state.delete_forward += after_length;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+163
View File
@@ -0,0 +1,163 @@
//! Wayland part of [`super`]: an own text-input object on the window's connection.
use std::ffi::c_void;
use wayland_client::{
delegate_noop,
protocol::{wl_registry, wl_seat},
Connection, Dispatch, EventQueue, QueueHandle,
};
use wayland_protocols::wp::text_input::zv3::client::{
zwp_text_input_manager_v3::ZwpTextInputManagerV3,
zwp_text_input_v3::{self, ZwpTextInputV3},
};
use super::deletion_key_events;
/// Platzhalter-Umgebungstext: ein Zeichen vor der Schreibmarke, damit die Rücktaste der
/// Bildschirmtastatur immer etwas zu löschen hat (ein Byte = ein Zeichen)
const SURROUNDING_PLACEHOLDER: &str = " ";
#[derive(Default)]
struct State {
seat: Option<wl_seat::WlSeat>,
manager: Option<ZwpTextInputManagerV3>,
text_input: Option<ZwpTextInputV3>,
/// Von der Bildschirmtastatur gewünschtes Löschen (Zeichen vor/nach der Schreibmarke)
delete_back: u32,
delete_forward: u32,
}
/// Eigenes text-input-Objekt, mit dem ink-notes die Bildschirmtastatur anfordert
pub struct KeyboardRequester {
conn: Connection,
queue: EventQueue<State>,
state: State,
}
impl KeyboardRequester {
/// Requester for the window if it runs on Wayland.
///
/// # Safety
/// The window must outlive the requester.
pub unsafe fn for_window(window: &winit::window::Window) -> Option<Self> {
use winit::raw_window_handle::{HasDisplayHandle, RawDisplayHandle};
match window.display_handle().map(|handle| handle.as_raw()) {
// Safety: passed on to the caller
Ok(RawDisplayHandle::Wayland(handle)) => {
Some(unsafe { Self::new(handle.display.as_ptr()) })
}
_ => None,
}
}
/// Hängt sich an die Wayland-Verbindung des Fensters.
///
/// # Safety
/// `wl_display` muss auf die `wl_display` des Fensters zeigen und gültig bleiben, solange
/// der `KeyboardRequester` lebt.
pub unsafe fn new(wl_display: *mut c_void) -> Self {
// Safety: an den Aufrufer weitergereicht
let backend =
unsafe { wayland_backend::client::Backend::from_foreign_display(wl_display.cast()) };
let conn = Connection::from_backend(backend);
let queue = conn.new_event_queue();
conn.display().get_registry(&queue.handle(), ());
let _ = conn.flush();
Self {
conn,
queue,
state: State::default(),
}
}
/// Verarbeitet eingetroffene Ereignisse; einmal pro Frame aufrufen.
pub fn pump(&mut self) {
let _ = self.queue.dispatch_pending(&mut self.state);
if self.state.text_input.is_none() {
if let (Some(manager), Some(seat)) = (&self.state.manager, &self.state.seat) {
self.state.text_input =
Some(manager.get_text_input(seat, &self.queue.handle(), ()));
}
}
}
/// Bittet den Compositor, die Bildschirmtastatur einzublenden. Muss nach dem `enable` von
/// winit kommen (also nach `handle_platform_output`).
pub fn request(&mut self) {
self.pump();
if let Some(text_input) = &self.state.text_input {
text_input.enable();
self.send_surrounding_placeholder();
}
}
/// Tastendrücke für das seit dem letzten Aufruf gewünschte Löschen (vor `begin_frame`
/// aufrufen und an die egui-Eingabe anhängen)
pub fn take_deletions(&mut self) -> Vec<egui::Event> {
self.pump();
// Den Platzhalter hier nicht neu melden: Die Tastatur löscht bei jeder neuen Meldung
// erneut, solange die Rücktaste gedrückt ist – ein kurzes Antippen löschte sonst doppelt.
let back = std::mem::take(&mut self.state.delete_back);
let forward = std::mem::take(&mut self.state.delete_forward);
deletion_key_events(back, forward)
}
fn send_surrounding_placeholder(&self) {
if let Some(text_input) = &self.state.text_input {
let len = SURROUNDING_PLACEHOLDER.len() as i32;
text_input.set_surrounding_text(SURROUNDING_PLACEHOLDER.to_owned(), len, len);
text_input.commit();
let _ = self.conn.flush();
}
}
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global {
name, interface, ..
} = event
{
match interface.as_str() {
"wl_seat" if state.seat.is_none() => {
state.seat = Some(registry.bind(name, 1, qh, ()));
}
"zwp_text_input_manager_v3" if state.manager.is_none() => {
state.manager = Some(registry.bind(name, 1, qh, ()));
}
_ => {}
}
}
}
}
delegate_noop!(State: ignore wl_seat::WlSeat);
delegate_noop!(State: ZwpTextInputManagerV3);
impl Dispatch<ZwpTextInputV3, ()> for State {
fn event(
state: &mut Self,
_: &ZwpTextInputV3,
event: zwp_text_input_v3::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
// Getippter Text kommt über winit; hier nur das Löschen
if let zwp_text_input_v3::Event::DeleteSurroundingText {
before_length,
after_length,
} = event
{
state.delete_back += before_length;
state.delete_forward += after_length;
}
}
}
+74
View File
@@ -0,0 +1,74 @@
//! Folders for notebooks, settings and downloads on each platform (Issues #42, #48).
//!
//! - Linux: XDG (`~/.local/share/ink-notes`, `~/.config/ink-notes`, `~/.cache/ink-notes`)
//! - Windows: `%APPDATA%\ink-notes` for data and settings, `%LOCALAPPDATA%\ink-notes` for
//! downloads
//! - Android: the app's internal folder, set once at startup with [`set_app_dir`]
//!
//! The environment is read on every call, so tests can point the folders elsewhere.
use std::path::PathBuf;
use std::sync::OnceLock;
/// Fixed app folder (Android: internal storage of the app)
static APP_DIR: OnceLock<PathBuf> = OnceLock::new();
/// Puts data, settings and downloads below `dir` (Android, where there is no XDG or home).
pub fn set_app_dir(dir: PathBuf) {
let _ = APP_DIR.set(dir);
}
/// Value of an environment variable, unless empty.
fn env_dir(key: &str) -> Option<PathBuf> {
std::env::var_os(key)
.filter(|v| !v.to_string_lossy().trim().is_empty())
.map(PathBuf::from)
}
/// Home folder of the user.
pub fn home_dir() -> Option<PathBuf> {
if cfg!(windows) {
env_dir("USERPROFILE").or_else(|| env_dir("HOME"))
} else {
env_dir("HOME")
}
}
/// Folder for notebooks and emergency backups.
pub fn data_dir() -> Option<PathBuf> {
if let Some(dir) = APP_DIR.get() {
return Some(dir.clone());
}
if cfg!(windows) {
return env_dir("APPDATA").map(|d| d.join("ink-notes"));
}
env_dir("XDG_DATA_HOME")
.or_else(|| home_dir().map(|h| h.join(".local").join("share")))
.map(|d| d.join("ink-notes"))
}
/// Folder for the settings (`ui.json`).
pub fn config_dir() -> Option<PathBuf> {
if let Some(dir) = APP_DIR.get() {
return Some(dir.join("config"));
}
if cfg!(windows) {
return env_dir("APPDATA").map(|d| d.join("ink-notes"));
}
env_dir("XDG_CONFIG_HOME")
.or_else(|| home_dir().map(|h| h.join(".config")))
.map(|d| d.join("ink-notes"))
}
/// Folder for downloaded updates.
pub fn cache_dir() -> Option<PathBuf> {
if let Some(dir) = APP_DIR.get() {
return Some(dir.join("cache"));
}
if cfg!(windows) {
return env_dir("LOCALAPPDATA").map(|d| d.join("ink-notes"));
}
env_dir("XDG_CACHE_HOME")
.or_else(|| home_dir().map(|h| h.join(".cache")))
.map(|d| d.join("ink-notes"))
}
+3 -3
View File
@@ -1,13 +1,13 @@
//! Legt beim PDF-Export die Originalseiten importierter PDFs als Vektorgrafik unter die
//! Notizen (Text bleibt scharf und auswählbar).
//!
//! Vorgehen: Das von printpdf erzeugte PDF wird mit `lopdf` geöffnet. Jede benötigte Seite
//! Vorgehen: Das vom Export erzeugte PDF wird mit `lopdf` geöffnet. Jede benötigte Seite
//! des Quell-PDFs wird als Form-XObject (Inhalt + Ressourcen der Seite) übernommen und am
//! Anfang der Zielseite gezeichnet, also unter Hintergrund, Bildern und Tinte.
use std::collections::{BTreeMap, HashMap};
use printpdf::lopdf::{Dictionary, Document, Object, ObjectId, Stream};
use lopdf::{Dictionary, Document, Object, ObjectId, Stream};
use crate::t;
@@ -361,7 +361,7 @@ mod tests {
2,
"Quellseiten dürfen nicht als eigene Seiten auftauchen"
);
let first = doc.get_page_content(pages[0]).unwrap();
let first = doc.get_page_content(pages[0]);
let text = String::from_utf8_lossy(&first);
assert!(
text.starts_with("q "),
+879
View File
@@ -0,0 +1,879 @@
//! Small PDF writer on top of `lopdf` for the vector export.
//!
//! Replaces printpdf 0.7, which still depended on lopdf 0.31. The interface follows the part
//! of printpdf that [`crate::export`] used: a document with pages, each page with named
//! layers (optional content groups, shown as layers in PDF viewers) that collect drawing
//! operations, embedded TrueType fonts for real, searchable text, images and bookmarks.
use std::cell::RefCell;
use std::collections::{BTreeMap, BTreeSet};
use std::error::Error;
use std::rc::Rc;
use ab_glyph::Font as _;
use lopdf::content::{Content, Operation};
use lopdf::{Dictionary, Document, Object, ObjectId, Stream, StringFormat};
/// Length in millimetres.
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Mm(pub f32);
/// Length in PDF points (1/72 inch).
#[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
pub struct Pt(pub f32);
impl From<Mm> for Pt {
fn from(mm: Mm) -> Self {
Pt(mm.0 * 72.0 / 25.4)
}
}
/// Size in pixels.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Px(pub usize);
/// Point on the page (origin bottom left).
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point {
pub x: Pt,
pub y: Pt,
}
impl Point {
pub fn new(x: Mm, y: Mm) -> Self {
Self {
x: x.into(),
y: y.into(),
}
}
}
/// Open or closed polyline; a point marked `true` followed by another marked `true` starts a
/// cubic Bézier segment (the two are its control points, the next one its end point).
#[derive(Debug, Clone)]
pub struct Line {
pub points: Vec<(Point, bool)>,
pub is_closed: bool,
}
/// How a polygon is painted.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PaintMode {
Fill,
Stroke,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum WindingOrder {
NonZero,
}
/// Closed shape of one or more rings (same point marking as [`Line`]).
#[derive(Debug, Clone)]
pub struct Polygon {
pub rings: Vec<Vec<(Point, bool)>>,
pub mode: PaintMode,
pub winding_order: WindingOrder,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Rgb {
pub r: f32,
pub g: f32,
pub b: f32,
}
impl Rgb {
/// The last argument (ICC profile in printpdf) is ignored.
pub fn new(r: f32, g: f32, b: f32, _icc: Option<()>) -> Self {
Self { r, g, b }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Color {
Rgb(Rgb),
}
/// Change of the coordinate system (`cm`).
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum CurTransMat {
Translate(Pt, Pt),
Raw([f32; 6]),
}
/// Text position within a text section (`Tm`).
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TextMatrix {
Translate(Pt, Pt),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineCapStyle {
Butt,
Round,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LineJoinStyle {
Miter,
Round,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColorSpace {
Rgb,
Greyscale,
}
/// Image data already compressed in this format (embedded unchanged).
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ImageFilter {
/// JPEG
DCT,
}
/// Image with 8 bits per component.
#[derive(Debug, Clone)]
pub struct ImageXObject {
pub width: Px,
pub height: Px,
pub color_space: ColorSpace,
pub interpolate: bool,
/// Raw samples, or the compressed data for `image_filter`
pub image_data: Vec<u8>,
pub image_filter: Option<ImageFilter>,
}
/// Font embedded with [`PdfDocumentReference::add_external_font`].
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct IndirectFontRef {
index: usize,
}
impl IndirectFontRef {
fn resource_name(&self) -> String {
format!("F{}", self.index)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PdfPageIndex(pub usize);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct PdfLayerIndex(pub usize);
struct LayerData {
name: String,
operations: Vec<Operation>,
/// Font of the current text section (to record the glyphs it uses)
current_font: Option<usize>,
}
struct PageData {
width: Pt,
height: Pt,
layers: Vec<LayerData>,
images: Vec<ImageXObject>,
}
struct FontData {
face: ab_glyph::FontRef<'static>,
bytes: &'static [u8],
used_glyphs: BTreeSet<u16>,
}
struct DocumentData {
title: String,
pages: Vec<PageData>,
fonts: Vec<FontData>,
bookmarks: Vec<(String, usize)>,
}
/// Entry point like printpdf's `PdfDocument::new`.
pub struct PdfDocument;
impl PdfDocument {
/// New document with a first page of the given size and its first layer.
#[allow(clippy::new_ret_no_self)]
pub fn new(
title: &str,
width: Mm,
height: Mm,
layer: &str,
) -> (PdfDocumentReference, PdfPageIndex, PdfLayerIndex) {
let doc = PdfDocumentReference(Rc::new(RefCell::new(DocumentData {
title: title.to_string(),
pages: Vec::new(),
fonts: Vec::new(),
bookmarks: Vec::new(),
})));
let (page, layer) = doc.add_page(width, height, layer);
(doc, page, layer)
}
}
/// The document being built; cheap to clone, all clones share it.
#[derive(Clone)]
pub struct PdfDocumentReference(Rc<RefCell<DocumentData>>);
impl PdfDocumentReference {
/// Appends a page with its first layer.
pub fn add_page(&self, width: Mm, height: Mm, layer: &str) -> (PdfPageIndex, PdfLayerIndex) {
let mut doc = self.0.borrow_mut();
doc.pages.push(PageData {
width: width.into(),
height: height.into(),
layers: vec![LayerData {
name: layer.to_string(),
operations: Vec::new(),
current_font: None,
}],
images: Vec::new(),
});
(PdfPageIndex(doc.pages.len() - 1), PdfLayerIndex(0))
}
/// Bookmark (outline entry) pointing at a page.
pub fn add_bookmark(&self, title: String, page: PdfPageIndex) {
self.0.borrow_mut().bookmarks.push((title, page.0));
}
pub fn get_page(&self, page: PdfPageIndex) -> PdfPageReference {
PdfPageReference {
doc: self.clone(),
page: page.0,
}
}
/// Embeds a TrueType/OpenType font with TrueType outlines (Type0 font, Identity-H).
pub fn add_external_font(
&self,
bytes: &'static [u8],
) -> Result<IndirectFontRef, Box<dyn Error>> {
let face = ab_glyph::FontRef::try_from_slice(bytes)?;
let mut doc = self.0.borrow_mut();
doc.fonts.push(FontData {
face,
bytes,
used_glyphs: BTreeSet::new(),
});
Ok(IndirectFontRef {
index: doc.fonts.len() - 1,
})
}
/// Writes the document.
pub fn save_to_bytes(self) -> Result<Vec<u8>, Box<dyn Error>> {
let data = self.0.borrow();
let mut doc = Document::with_version("1.5");
let pages_id = doc.new_object_id();
// Fonts: shared by all pages
let mut font_resources = Dictionary::new();
for (index, font) in data.fonts.iter().enumerate() {
let id = write_font(&mut doc, font, index)?;
font_resources.set(format!("F{index}"), Object::Reference(id));
}
// One optional content group per layer name, in the order of appearance
let mut layer_names: Vec<&str> = Vec::new();
for layer in data.pages.iter().flat_map(|p| &p.layers) {
if !layer_names.contains(&layer.name.as_str()) {
layer_names.push(&layer.name);
}
}
let ocgs: Vec<ObjectId> = layer_names
.iter()
.map(|name| {
let mut ocg = Dictionary::new();
ocg.set("Type", Object::Name(b"OCG".to_vec()));
ocg.set("Name", text_string(name));
doc.add_object(ocg)
})
.collect();
let mut page_ids = Vec::new();
for page in &data.pages {
let mut operations = Vec::new();
let mut properties = Dictionary::new();
for layer in &page.layers {
let index = layer_names
.iter()
.position(|n| *n == layer.name)
.unwrap_or(0);
let tag = format!("MC{index}");
properties.set(tag.clone(), Object::Reference(ocgs[index]));
operations.push(Operation::new(
"BDC",
vec![Object::Name(b"OC".to_vec()), Object::Name(tag.into_bytes())],
));
operations.push(Operation::new("q", vec![]));
operations.extend(layer.operations.iter().cloned());
operations.push(Operation::new("Q", vec![]));
operations.push(Operation::new("EMC", vec![]));
}
let mut contents = Stream::new(Dictionary::new(), Content { operations }.encode()?);
contents.compress()?;
let contents_id = doc.add_object(contents);
let mut xobjects = Dictionary::new();
for (i, image) in page.images.iter().enumerate() {
let id = doc.add_object(image_stream(image)?);
xobjects.set(format!("X{i}"), Object::Reference(id));
}
let mut resources = Dictionary::new();
resources.set("Font", Object::Dictionary(font_resources.clone()));
resources.set("XObject", Object::Dictionary(xobjects));
resources.set("Properties", Object::Dictionary(properties));
let resources_id = doc.add_object(resources);
let mut dict = Dictionary::new();
dict.set("Type", Object::Name(b"Page".to_vec()));
dict.set("Parent", Object::Reference(pages_id));
dict.set(
"MediaBox",
Object::Array(vec![
0.into(),
0.into(),
page.width.0.into(),
page.height.0.into(),
]),
);
dict.set("Contents", Object::Reference(contents_id));
dict.set("Resources", Object::Reference(resources_id));
page_ids.push(doc.add_object(dict));
}
let mut pages = Dictionary::new();
pages.set("Type", Object::Name(b"Pages".to_vec()));
pages.set(
"Kids",
Object::Array(page_ids.iter().map(|id| Object::Reference(*id)).collect()),
);
pages.set("Count", Object::Integer(page_ids.len() as i64));
doc.objects.insert(pages_id, Object::Dictionary(pages));
let mut catalog = Dictionary::new();
catalog.set("Type", Object::Name(b"Catalog".to_vec()));
catalog.set("Pages", Object::Reference(pages_id));
let ocg_refs: Vec<Object> = ocgs.iter().map(|id| Object::Reference(*id)).collect();
let mut default_config = Dictionary::new();
default_config.set("Order", Object::Array(ocg_refs.clone()));
let mut oc_properties = Dictionary::new();
oc_properties.set("OCGs", Object::Array(ocg_refs));
oc_properties.set("D", Object::Dictionary(default_config));
catalog.set("OCProperties", Object::Dictionary(oc_properties));
if !data.bookmarks.is_empty() {
let outlines = write_outlines(&mut doc, &data.bookmarks, &page_ids);
catalog.set("Outlines", Object::Reference(outlines));
catalog.set("PageMode", Object::Name(b"UseOutlines".to_vec()));
}
let catalog_id = doc.add_object(catalog);
let mut info = Dictionary::new();
info.set("Title", text_string(&data.title));
info.set("Producer", text_string("ink-notes"));
let info_id = doc.add_object(info);
doc.trailer.set("Root", Object::Reference(catalog_id));
doc.trailer.set("Info", Object::Reference(info_id));
let mut out = Vec::new();
doc.save_to(&mut out)?;
Ok(out)
}
}
/// PDF text string: literal for ASCII, otherwise UTF-16BE with byte order mark.
pub fn text_string(text: &str) -> Object {
if text.is_ascii() {
return Object::String(text.as_bytes().to_vec(), StringFormat::Literal);
}
let mut bytes = vec![0xFE, 0xFF];
for unit in text.encode_utf16() {
bytes.extend_from_slice(&unit.to_be_bytes());
}
Object::String(bytes, StringFormat::Hexadecimal)
}
/// Outline tree with one entry per bookmark; returns the outline root.
fn write_outlines(
doc: &mut Document,
bookmarks: &[(String, usize)],
pages: &[ObjectId],
) -> ObjectId {
let root_id = doc.new_object_id();
let ids: Vec<ObjectId> = bookmarks.iter().map(|_| doc.new_object_id()).collect();
for (i, (title, page)) in bookmarks.iter().enumerate() {
let mut item = Dictionary::new();
item.set("Title", text_string(title));
item.set("Parent", Object::Reference(root_id));
if i > 0 {
item.set("Prev", Object::Reference(ids[i - 1]));
}
if let Some(next) = ids.get(i + 1) {
item.set("Next", Object::Reference(*next));
}
if let Some(page_id) = pages.get(*page) {
item.set(
"Dest",
Object::Array(vec![
Object::Reference(*page_id),
Object::Name(b"Fit".to_vec()),
]),
);
}
doc.objects.insert(ids[i], Object::Dictionary(item));
}
let mut root = Dictionary::new();
root.set("Type", Object::Name(b"Outlines".to_vec()));
root.set("First", Object::Reference(ids[0]));
root.set("Last", Object::Reference(ids[ids.len() - 1]));
root.set("Count", Object::Integer(ids.len() as i64));
doc.objects.insert(root_id, Object::Dictionary(root));
root_id
}
/// Image XObject stream.
fn image_stream(image: &ImageXObject) -> Result<Stream, Box<dyn Error>> {
let mut dict = Dictionary::new();
dict.set("Type", Object::Name(b"XObject".to_vec()));
dict.set("Subtype", Object::Name(b"Image".to_vec()));
dict.set("Width", Object::Integer(image.width.0 as i64));
dict.set("Height", Object::Integer(image.height.0 as i64));
let color_space: &[u8] = match image.color_space {
ColorSpace::Rgb => b"DeviceRGB",
ColorSpace::Greyscale => b"DeviceGray",
};
dict.set("ColorSpace", Object::Name(color_space.to_vec()));
dict.set("BitsPerComponent", Object::Integer(8));
dict.set("Interpolate", Object::Boolean(image.interpolate));
match image.image_filter {
Some(ImageFilter::DCT) => {
dict.set("Filter", Object::Name(b"DCTDecode".to_vec()));
Ok(Stream::new(dict, image.image_data.clone()).with_compression(false))
}
None => {
let mut stream = Stream::new(dict, image.image_data.clone());
stream.compress()?;
Ok(stream)
}
}
}
/// Embeds a font as Type0 font with a CIDFontType2 descendant (glyph ids as character codes),
/// widths and a ToUnicode map of the glyphs used, so that the text can be searched and copied.
fn write_font(
doc: &mut Document,
font: &FontData,
index: usize,
) -> Result<ObjectId, Box<dyn Error>> {
let face = &font.face;
let units_per_em = face.units_per_em().unwrap_or(1000.0);
let scale = 1000.0 / units_per_em;
let name = format!("InkNotesFont{index}");
let mut file = Stream::new(
Dictionary::from_iter(vec![("Length1", Object::Integer(font.bytes.len() as i64))]),
font.bytes.to_vec(),
);
file.compress()?;
let file_id = doc.add_object(file);
let ascent = (face.ascent_unscaled() * scale).round() as i64;
let descent = (face.descent_unscaled() * scale).round() as i64;
let mut descriptor = Dictionary::new();
descriptor.set("Type", Object::Name(b"FontDescriptor".to_vec()));
descriptor.set("FontName", Object::Name(name.clone().into_bytes()));
descriptor.set("Flags", Object::Integer(32));
descriptor.set(
"FontBBox",
Object::Array(vec![0.into(), descent.into(), 1000.into(), ascent.into()]),
);
descriptor.set("ItalicAngle", Object::Integer(0));
descriptor.set("Ascent", Object::Integer(ascent));
descriptor.set("Descent", Object::Integer(descent));
descriptor.set("CapHeight", Object::Integer(ascent));
descriptor.set("StemV", Object::Integer(80));
descriptor.set("FontFile2", Object::Reference(file_id));
let descriptor_id = doc.add_object(descriptor);
// Widths of the glyphs used: gid [width]
let mut widths = Vec::new();
for &gid in &font.used_glyphs {
let width = face.h_advance_unscaled(ab_glyph::GlyphId(gid)) * scale;
widths.push(Object::Integer(gid as i64));
widths.push(Object::Array(vec![Object::Integer(width.round() as i64)]));
}
let mut system_info = Dictionary::new();
system_info.set("Registry", Object::string_literal("Adobe"));
system_info.set("Ordering", Object::string_literal("Identity"));
system_info.set("Supplement", Object::Integer(0));
let mut descendant = Dictionary::new();
descendant.set("Type", Object::Name(b"Font".to_vec()));
descendant.set("Subtype", Object::Name(b"CIDFontType2".to_vec()));
descendant.set("BaseFont", Object::Name(name.clone().into_bytes()));
descendant.set("CIDSystemInfo", Object::Dictionary(system_info));
descendant.set("FontDescriptor", Object::Reference(descriptor_id));
descendant.set("CIDToGIDMap", Object::Name(b"Identity".to_vec()));
descendant.set("DW", Object::Integer(1000));
descendant.set("W", Object::Array(widths));
let descendant_id = doc.add_object(descendant);
let mut to_unicode = Stream::new(Dictionary::new(), to_unicode_cmap(font).into_bytes());
to_unicode.compress()?;
let to_unicode_id = doc.add_object(to_unicode);
let mut dict = Dictionary::new();
dict.set("Type", Object::Name(b"Font".to_vec()));
dict.set("Subtype", Object::Name(b"Type0".to_vec()));
dict.set("BaseFont", Object::Name(name.into_bytes()));
dict.set("Encoding", Object::Name(b"Identity-H".to_vec()));
dict.set(
"DescendantFonts",
Object::Array(vec![Object::Reference(descendant_id)]),
);
dict.set("ToUnicode", Object::Reference(to_unicode_id));
Ok(doc.add_object(dict))
}
/// CMap from the glyph ids used to their characters (UTF-16BE).
fn to_unicode_cmap(font: &FontData) -> String {
let mut chars: BTreeMap<u16, char> = BTreeMap::new();
for (gid, c) in font.face.codepoint_ids() {
if font.used_glyphs.contains(&gid.0) {
// Prefer the lowest code point (e.g. a letter over a compatibility form)
let entry = chars.entry(gid.0).or_insert(c);
if c < *entry {
*entry = c;
}
}
}
let mut cmap = String::from(
"/CIDInit /ProcSet findresource begin\n12 dict begin\nbegincmap\n\
/CIDSystemInfo << /Registry (Adobe) /Ordering (UCS) /Supplement 0 >> def\n\
/CMapName /Adobe-Identity-UCS def\n/CMapType 2 def\n\
1 begincodespacerange\n<0000> <FFFF>\nendcodespacerange\n",
);
let entries: Vec<(u16, char)> = chars.into_iter().collect();
for block in entries.chunks(100) {
cmap.push_str(&format!("{} beginbfchar\n", block.len()));
for (gid, c) in block {
let mut units = [0u16; 2];
let hex: String = c
.encode_utf16(&mut units)
.iter()
.map(|u| format!("{u:04X}"))
.collect();
cmap.push_str(&format!("<{gid:04X}> <{hex}>\n"));
}
cmap.push_str("endbfchar\n");
}
cmap.push_str("endcmap\nCMapName currentdict /CMap defineresource pop\nend\nend\n");
cmap
}
pub struct PdfPageReference {
doc: PdfDocumentReference,
page: usize,
}
impl PdfPageReference {
pub fn get_layer(&self, layer: PdfLayerIndex) -> PdfLayerReference {
PdfLayerReference {
doc: self.doc.clone(),
page: self.page,
layer: layer.0,
}
}
/// Adds a layer drawn above the existing ones.
pub fn add_layer(&self, name: &str) -> PdfLayerReference {
let mut doc = self.doc.0.borrow_mut();
let layers = &mut doc.pages[self.page].layers;
layers.push(LayerData {
name: name.to_string(),
operations: Vec::new(),
current_font: None,
});
PdfLayerReference {
doc: self.doc.clone(),
page: self.page,
layer: layers.len() - 1,
}
}
}
/// A layer of a page; all drawing goes through it.
#[derive(Clone)]
pub struct PdfLayerReference {
doc: PdfDocumentReference,
page: usize,
layer: usize,
}
impl PdfLayerReference {
fn with_layer<T>(&self, f: impl FnOnce(&mut LayerData) -> T) -> T {
let mut doc = self.doc.0.borrow_mut();
f(&mut doc.pages[self.page].layers[self.layer])
}
fn push(&self, operator: &str, operands: Vec<Object>) {
self.add_operation(Operation::new(operator, operands));
}
pub fn add_operation(&self, operation: Operation) {
self.with_layer(|layer| layer.operations.push(operation));
}
pub fn save_graphics_state(&self) {
self.push("q", vec![]);
}
pub fn restore_graphics_state(&self) {
self.push("Q", vec![]);
}
pub fn set_ctm(&self, matrix: CurTransMat) {
let m = match matrix {
CurTransMat::Translate(x, y) => [1.0, 0.0, 0.0, 1.0, x.0, y.0],
CurTransMat::Raw(m) => m,
};
self.push("cm", m.iter().map(|&v| v.into()).collect());
}
pub fn set_fill_color(&self, color: Color) {
let Color::Rgb(c) = color;
self.push("rg", vec![c.r.into(), c.g.into(), c.b.into()]);
}
pub fn set_outline_color(&self, color: Color) {
let Color::Rgb(c) = color;
self.push("RG", vec![c.r.into(), c.g.into(), c.b.into()]);
}
pub fn set_outline_thickness(&self, thickness: f32) {
self.push("w", vec![thickness.into()]);
}
pub fn set_line_cap_style(&self, style: LineCapStyle) {
let value = match style {
LineCapStyle::Butt => 0,
LineCapStyle::Round => 1,
};
self.push("J", vec![Object::Integer(value)]);
}
pub fn set_line_join_style(&self, style: LineJoinStyle) {
let value = match style {
LineJoinStyle::Miter => 0,
LineJoinStyle::Round => 1,
};
self.push("j", vec![Object::Integer(value)]);
}
pub fn add_line(&self, line: Line) {
if line.points.is_empty() {
return;
}
let mut operations = path_operations(&line.points);
operations.push(Operation::new(
if line.is_closed { "s" } else { "S" },
vec![],
));
self.with_layer(|layer| layer.operations.extend(operations));
}
pub fn add_polygon(&self, polygon: Polygon) {
let mut operations: Vec<Operation> = polygon
.rings
.iter()
.filter(|ring| !ring.is_empty())
.flat_map(|ring| path_operations(ring))
.collect();
if operations.is_empty() {
return;
}
operations.push(Operation::new(
match polygon.mode {
PaintMode::Fill => "f",
PaintMode::Stroke => "s",
},
vec![],
));
self.with_layer(|layer| layer.operations.extend(operations));
}
/// Draws `image` into the unit square of the current coordinate system.
pub fn add_image(&self, image: ImageXObject) {
let mut doc = self.doc.0.borrow_mut();
let page = &mut doc.pages[self.page];
page.images.push(image);
let name = format!("X{}", page.images.len() - 1);
page.layers[self.layer]
.operations
.push(Operation::new("Do", vec![Object::Name(name.into_bytes())]));
}
pub fn begin_text_section(&self) {
self.push("BT", vec![]);
}
pub fn end_text_section(&self) {
self.push("ET", vec![]);
self.with_layer(|layer| layer.current_font = None);
}
pub fn set_font(&self, font: &IndirectFontRef, size: f32) {
self.push(
"Tf",
vec![Object::Name(font.resource_name().into_bytes()), size.into()],
);
self.with_layer(|layer| layer.current_font = Some(font.index));
}
pub fn set_text_matrix(&self, matrix: TextMatrix) {
let TextMatrix::Translate(x, y) = matrix;
self.push(
"Tm",
vec![
1.into(),
0.into(),
0.into(),
1.into(),
x.0.into(),
y.0.into(),
],
);
}
/// Shows glyphs of the current font; each comes with a shift before it in 1/1000 em
/// (positive moves left, as in `TJ`).
pub fn write_positioned_codepoints(&self, glyphs: impl IntoIterator<Item = (i64, u16)>) {
let mut list = Vec::new();
let mut used = Vec::new();
for (shift, gid) in glyphs {
if shift != 0 {
list.push(Object::Integer(shift));
}
list.push(Object::String(
gid.to_be_bytes().to_vec(),
StringFormat::Hexadecimal,
));
used.push(gid);
}
let mut doc = self.doc.0.borrow_mut();
let layer = &mut doc.pages[self.page].layers[self.layer];
layer
.operations
.push(Operation::new("TJ", vec![Object::Array(list)]));
if let Some(font) = layer.current_font {
doc.fonts[font].used_glyphs.extend(used);
}
}
}
/// `m`, `l` and `c` operations for a point list (see [`Line`] for the Bézier marking).
fn path_operations(points: &[(Point, bool)]) -> Vec<Operation> {
let xy = |p: &Point| -> [Object; 2] { [p.x.0.into(), p.y.0.into()] };
let mut operations = vec![Operation::new("m", xy(&points[0].0).to_vec())];
let mut current = 1;
while current < points.len() {
let (previous, point) = (&points[current - 1], &points[current]);
if previous.1 && point.1 {
if let (Some(c2), Some(end)) = (points.get(current + 1), points.get(current + 2)) {
let operands = [xy(&point.0), xy(&c2.0), xy(&end.0)].concat();
operations.push(Operation::new("c", operands));
current += 3;
continue;
}
}
operations.push(Operation::new("l", xy(&point.0).to_vec()));
current += 1;
}
operations
}
#[cfg(test)]
mod tests {
use super::*;
fn line(points: &[(f32, f32, bool)]) -> Vec<(Point, bool)> {
points
.iter()
.map(|&(x, y, b)| (Point::new(Mm(x), Mm(y)), b))
.collect()
}
#[test]
fn test_path_operations_lines_and_curves() {
let ops = path_operations(&line(&[
(0.0, 0.0, true),
(1.0, 0.0, true),
(2.0, 0.0, false),
(3.0, 0.0, false),
(4.0, 0.0, false),
]));
let names: Vec<&str> = ops.iter().map(|o| o.operator.as_str()).collect();
assert_eq!(names, ["m", "c", "l"]);
assert_eq!(ops[1].operands.len(), 6);
}
#[test]
fn test_document_with_text_image_and_bookmarks() {
let (doc, page, layer) = PdfDocument::new("Test", Mm(210.0), Mm(297.0), "Background");
let font = doc
.add_external_font(crate::text::TextFont::Sans.bytes())
.unwrap();
doc.add_bookmark("Übung – 1".to_string(), page);
let background = doc.get_page(page).get_layer(layer);
background.add_polygon(Polygon {
rings: vec![line(&[
(0.0, 0.0, false),
(10.0, 0.0, false),
(10.0, 10.0, false),
])],
mode: PaintMode::Fill,
winding_order: WindingOrder::NonZero,
});
let text = doc.get_page(page).add_layer("Text");
let face = crate::text::TextFont::Sans.face();
let gids: Vec<u16> = "Hä".chars().map(|c| face.glyph_id(c).0).collect();
text.begin_text_section();
text.set_font(&font, 12.0);
text.write_positioned_codepoints(gids.iter().map(|&g| (0, g)));
text.end_text_section();
text.add_image(ImageXObject {
width: Px(1),
height: Px(1),
color_space: ColorSpace::Rgb,
interpolate: true,
image_data: vec![255, 0, 0],
image_filter: None,
});
let bytes = doc.save_to_bytes().unwrap();
let pdf = Document::load_mem(&bytes).unwrap();
assert_eq!(pdf.get_pages().len(), 1);
let page_id = *pdf.get_pages().values().next().unwrap();
let content = pdf.get_page_content(page_id);
let ops = Content::decode(&content).unwrap().operations;
let names: Vec<&str> = ops.iter().map(|o| o.operator.as_str()).collect();
for expected in ["BDC", "f", "BT", "Tf", "TJ", "ET", "Do", "EMC"] {
assert!(names.contains(&expected), "{expected} missing in {names:?}");
}
// The ToUnicode map knows both characters
let fonts = pdf.get_page_fonts(page_id).unwrap();
let font = fonts.values().next().unwrap();
let to_unicode = pdf
.get_object(font.get(b"ToUnicode").unwrap().as_reference().unwrap())
.unwrap()
.as_stream()
.unwrap()
.decompressed_content()
.unwrap();
let cmap = String::from_utf8(to_unicode).unwrap();
assert!(cmap.contains("<0048>"), "{cmap}");
assert!(cmap.contains("<00E4>"), "{cmap}");
}
}
+113
View File
@@ -58,6 +58,69 @@ pub fn display_backend_from_env() -> DisplayBackend {
)
}
/// Event loop for the desktop; on Linux with the display backend chosen above.
#[cfg(not(target_os = "android"))]
pub fn desktop_event_loop() -> Result<winit::event_loop::EventLoop<()>, winit::error::EventLoopError>
{
#[allow(unused_mut)]
let mut builder = winit::event_loop::EventLoopBuilder::new();
#[cfg(target_os = "linux")]
{
use winit::platform::{wayland::EventLoopBuilderExtWayland, x11::EventLoopBuilderExtX11};
match display_backend_from_env() {
DisplayBackend::X11 => {
eprintln!("[Anzeige] Verwende X11 (unter WSL Standard; übersteuerbar mit INK_NOTES_DISPLAY=wayland)");
builder.with_x11();
}
DisplayBackend::Wayland => {
eprintln!("[Anzeige] Verwende Wayland (INK_NOTES_DISPLAY=wayland)");
builder.with_wayland();
}
DisplayBackend::Automatic => {}
}
}
builder.build()
}
/// Gives the window the app id (Wayland: app_id, X11: WM_CLASS), so that dock and task bar
/// show the app icon for it.
pub fn with_app_id(
builder: winit::window::WindowBuilder,
#[allow(unused_variables)] app_id: &str,
) -> winit::window::WindowBuilder {
#[cfg(target_os = "linux")]
{
let builder = winit::platform::wayland::WindowBuilderExtWayland::with_name(
builder,
app_id,
"ink-notes",
);
winit::platform::x11::WindowBuilderExtX11::with_name(builder, app_id, "ink-notes")
}
#[cfg(not(target_os = "linux"))]
builder
}
/// Ignores touches on the client-side title bar until `until` (palm rejection while the pen
/// is near; only Wayland draws its own title bar).
pub fn ignore_decoration_touches_until(
#[allow(unused_variables)] until: Option<std::time::Instant>,
) {
#[cfg(target_os = "linux")]
winit::platform::wayland::ignore_decoration_touches_until(until);
}
/// Time of a touch event as reported by the compositor (milliseconds, Wayland only).
pub fn take_touch_time(
#[allow(unused_variables)] id: u64,
#[allow(unused_variables)] phase: winit::event::TouchPhase,
) -> Option<u32> {
#[cfg(target_os = "linux")]
return winit::platform::wayland::take_touch_time(id, phase);
#[cfg(not(target_os = "linux"))]
None
}
/// Bringt die Ereignisse eines Tablet-Frames in eine brauchbare Reihenfolge: octotablet meldet
/// das Aufsetzen des Stifts (`Down`) vor der Position desselben Frames (`Pose`). Bis dahin
/// gälte noch die alte Position – kam der Stift schnell heran (Annähern und Aufsetzen im selben
@@ -91,6 +154,56 @@ pub fn down_after_pose<T>(
ordered
}
/// System clipboard: arboard on the desktop; on Android not available yet (Issue #42).
pub mod clipboard {
#[cfg(not(target_os = "android"))]
pub use arboard::{Clipboard, ImageData};
#[cfg(target_os = "android")]
pub use android::{Clipboard, ImageData};
#[cfg(target_os = "android")]
mod android {
use std::borrow::Cow;
pub struct ImageData<'a> {
pub width: usize,
pub height: usize,
pub bytes: Cow<'a, [u8]>,
}
impl ImageData<'_> {
pub fn to_owned_img(&self) -> ImageData<'static> {
ImageData {
width: self.width,
height: self.height,
bytes: Cow::Owned(self.bytes.to_vec()),
}
}
}
pub struct Clipboard;
impl Clipboard {
pub fn new() -> Result<Self, &'static str> {
Err("not available on Android")
}
pub fn get_image(&mut self) -> Result<ImageData<'static>, &'static str> {
Err("not available on Android")
}
pub fn get_text(&mut self) -> Result<String, &'static str> {
Err("not available on Android")
}
pub fn set_image(&mut self, _image: ImageData) -> Result<(), &'static str> {
Err("not available on Android")
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
+2 -6
View File
@@ -258,13 +258,9 @@ impl Default for UiSettings {
}
impl UiSettings {
/// Standardpfad gemäß XDG-Spezifikation.
/// Standardpfad gemäß XDG-Spezifikation (see [`crate::paths`] for other systems).
pub fn default_path() -> Option<PathBuf> {
let base = match std::env::var("XDG_CONFIG_HOME") {
Ok(dir) if !dir.trim().is_empty() => PathBuf::from(dir),
_ => PathBuf::from(std::env::var("HOME").ok()?).join(".config"),
};
Some(base.join("ink-notes").join("ui.json"))
crate::paths::config_dir().map(|d| d.join("ui.json"))
}
/// Lädt die Einstellungen; bei fehlender oder beschädigter Datei gelten Standardwerte.
+8 -16
View File
@@ -994,21 +994,9 @@ fn copy_dir(from: &Path, to: &Path) -> io::Result<()> {
/// Gibt das Standard-Verzeichnis für Notizbücher zurück (gemäß XDG-Spezifikation).
pub fn default_storage_dir() -> PathBuf {
if let Ok(xdg) = std::env::var("XDG_DATA_HOME") {
if !xdg.is_empty() {
return PathBuf::from(xdg).join("ink-notes").join("notebooks");
}
}
if let Ok(home) = std::env::var("HOME") {
if !home.is_empty() {
return PathBuf::from(home)
.join(".local")
.join("share")
.join("ink-notes")
.join("notebooks");
}
}
PathBuf::from("./data/notebooks")
crate::paths::data_dir()
.map(|d| d.join("notebooks"))
.unwrap_or_else(|| PathBuf::from("./data/notebooks"))
}
/// Status eines Notizbuchs in der Dokumentenliste.
@@ -1130,7 +1118,11 @@ impl NotebookLock {
}
Err(e) => {
let kind = e.kind();
if kind == io::ErrorKind::WouldBlock || e.raw_os_error() == Some(11) {
// Windows reports a held lock as ERROR_LOCK_VIOLATION
if kind == io::ErrorKind::WouldBlock
|| e.raw_os_error() == Some(11)
|| e.raw_os_error() == fs2::lock_contended_error().raw_os_error()
{
Err(StorageError::AlreadyLocked(t!(
"Notizbuch '{path}' wird bereits von einer anderen Instanz verwendet",
path = notebook_dir.display()
+2
View File
@@ -5076,6 +5076,7 @@ pub fn render_ui(
let is_recommended = match updater.runtime() {
crate::updater::RuntimeEnvironment::AppImage { .. } => is_appimage,
crate::updater::RuntimeEnvironment::DebPackage { .. } => is_deb,
crate::updater::RuntimeEnvironment::WindowsExe { .. } => asset.name.ends_with(".exe"),
_ => false,
};
@@ -5141,6 +5142,7 @@ pub fn render_ui(
crate::updater::RuntimeEnvironment::DebPackage { .. } => t!("📦 Paket mit Software-Center öffnen / installieren"),
crate::updater::RuntimeEnvironment::PortableOrDev { .. }
| crate::updater::RuntimeEnvironment::Flatpak => t!("📂 Im Dateimanager anzeigen"),
crate::updater::RuntimeEnvironment::WindowsExe { .. } => t!("🔄 Update installieren"),
};
ui.horizontal(|ui| {
+80 -19
View File
@@ -121,6 +121,8 @@ pub enum RuntimeEnvironment {
/// Als Flatpak installiert: Updates kommen über die Software-Verwaltung, ink-notes lädt
/// selbst nichts herunter (Vorgabe von Flathub)
Flatpak,
/// Portable `.exe` on Windows (Issue #48), replaced in place like the AppImage
WindowsExe { exe_path: PathBuf },
}
impl RuntimeEnvironment {
@@ -132,8 +134,13 @@ impl RuntimeEnvironment {
) {
return RuntimeEnvironment::Flatpak;
}
let appimage_var = std::env::var("APPIMAGE").ok();
let current_exe = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("ink-notes"));
if cfg!(windows) {
return RuntimeEnvironment::WindowsExe {
exe_path: current_exe,
};
}
let appimage_var = std::env::var("APPIMAGE").ok();
Self::detect_from(appimage_var.as_deref(), &current_exe)
}
@@ -166,6 +173,7 @@ impl RuntimeEnvironment {
RuntimeEnvironment::DebPackage { .. } => t!("System-Paket (.deb)"),
RuntimeEnvironment::PortableOrDev { .. } => t!("Entwicklung / Lokal"),
RuntimeEnvironment::Flatpak => "Flatpak",
RuntimeEnvironment::WindowsExe { .. } => "Windows",
}
}
@@ -210,18 +218,9 @@ pub fn sanitize_asset_name(raw_name: &str) -> Result<String, String> {
/// Liefert das isolierte Cache-Verzeichnis für Downloads pro Nutzer mit Rechten 0700.
pub fn get_updates_cache_dir() -> Result<PathBuf, String> {
let base = if let Ok(xdg) = std::env::var("XDG_CACHE_HOME") {
let trimmed = xdg.trim();
if !trimmed.is_empty() {
PathBuf::from(trimmed)
} else {
default_user_cache_dir()?
}
} else {
default_user_cache_dir()?
};
let dir = base.join("ink-notes").join("updates");
let dir = crate::paths::cache_dir()
.ok_or_else(|| t!("HOME-Umgebungsvariable ist nicht gesetzt.").to_string())?
.join("updates");
std::fs::create_dir_all(&dir).map_err(|e| {
t!(
"Konnte Update-Cache-Verzeichnis nicht anlegen ({path}): {e}",
@@ -239,12 +238,6 @@ pub fn get_updates_cache_dir() -> Result<PathBuf, String> {
Ok(dir)
}
fn default_user_cache_dir() -> Result<PathBuf, String> {
let home = std::env::var("HOME")
.map_err(|_| t!("HOME-Umgebungsvariable ist nicht gesetzt.").to_string())?;
Ok(PathBuf::from(home).join(".cache"))
}
/// Löscht alte temporäre Downloads aus dem Cache-Verzeichnis.
pub fn cleanup_old_updates() {
if let Ok(dir) = get_updates_cache_dir() {
@@ -551,6 +544,48 @@ fn http_get_with_auth_guard(
Err(t!("Zu viele Weiterleitungen (Redirect-Schleife abgebrochen).").to_string())
}
/// Replaces the running `.exe` with the downloaded one (Windows). A running program can't be
/// overwritten but can be renamed: the old one becomes `<name>.bak`, the new one takes its
/// place and starts with the next launch.
pub fn replace_running_exe(downloaded: &Path, exe_path: &Path) -> Result<(), String> {
let file_name = exe_path
.file_name()
.ok_or_else(|| t!("Ungültiger Programmpfad.").to_string())?
.to_string_lossy()
.into_owned();
let dir = exe_path
.parent()
.ok_or_else(|| t!("Ungültiger Programmpfad.").to_string())?;
let new_file = dir.join(format!("{file_name}.new"));
let backup = dir.join(format!("{file_name}.bak"));
std::fs::copy(downloaded, &new_file).map_err(|e| {
let _ = std::fs::remove_file(&new_file);
t!("Konnte temporäre Update-Datei nicht schreiben: {e}", e = e)
})?;
// A backup of an earlier update is no longer running
let _ = std::fs::remove_file(&backup);
if let Err(e) = std::fs::rename(exe_path, &backup) {
let _ = std::fs::remove_file(&new_file);
return Err(t!(
"Konnte Backup ({path}) nicht anlegen: {e}. Update abgebrochen, altes Programm unverändert.",
path = backup.display(),
e = e
));
}
if let Err(e) = std::fs::rename(&new_file, exe_path) {
// Put the old program back
let _ = std::fs::rename(&backup, exe_path);
let _ = std::fs::remove_file(&new_file);
return Err(t!(
"Konnte Programm nicht ersetzen: {e}. Altes Programm bleibt unverändert.",
e = e
));
}
let _ = std::fs::remove_file(downloaded);
Ok(())
}
/// Asynchroner Controller für Update-Prüfung und Asset-Download.
pub struct UpdaterController {
config: UpdaterConfig,
@@ -915,6 +950,7 @@ impl UpdaterController {
"Die Flatpak-Version wird über die Software-Verwaltung aktualisiert."
)
.to_string()),
RuntimeEnvironment::WindowsExe { exe_path } => replace_running_exe(file_path, exe_path),
}
}
@@ -1102,6 +1138,31 @@ dcdeb7ac87d9e6cb8022e5a71bc7f2c56d779b4fce4efdd5bae5be3b0214884a ink-notes_0.1.
);
}
#[test]
fn test_replace_running_exe() {
let dir = tempfile::tempdir().unwrap();
let exe = dir.path().join("ink-notes.exe");
let downloaded = dir.path().join("ink-notes_9.9.9_x86_64.exe");
std::fs::write(&exe, b"old").unwrap();
std::fs::write(&downloaded, b"new").unwrap();
replace_running_exe(&downloaded, &exe).unwrap();
assert_eq!(std::fs::read(&exe).unwrap(), b"new");
assert_eq!(
std::fs::read(dir.path().join("ink-notes.exe.bak")).unwrap(),
b"old"
);
assert!(!downloaded.exists());
// Again: the backup of the first update is replaced
std::fs::write(&downloaded, b"newer").unwrap();
replace_running_exe(&downloaded, &exe).unwrap();
assert_eq!(std::fs::read(&exe).unwrap(), b"newer");
assert_eq!(
std::fs::read(dir.path().join("ink-notes.exe.bak")).unwrap(),
b"new"
);
}
#[test]
fn test_flatpak_detection_disables_self_update() {
assert!(is_flatpak(Some("eu.pansi.ink-notes"), false));