Implement comprehensive metadata extraction for raster images with

EXIF support and display in the right panel.

New features:
- Extract basic metadata (filename, format, resolution, file size, color type)
- Parse EXIF data (camera, date, exposure, aperture, ISO, focal length, GPS)
- Display metadata in collapsible right panel (toggle with 'i' key)
- Auto-refresh metadata on document navigation

Changes by file:

Cargo.toml, Cargo.lock:
- Add kamadak-exif dependency for EXIF parsing

i18n/en/noctua.ftl:
- Add translation strings for all metadata labels

src/app/document/meta.rs:
- New module for metadata types (BasicMeta, ExifMeta, DocumentMeta)
- Extraction logic with EXIF parsing via kamadak-exif
- Helper methods for formatted display (resolution, file size, camera, GPS)

src/app/document/mod.rs:
- Re-export meta module

src/app/document/{raster,vector,portable}.rs:
- Add extract_metadata() method stubs (full impl for raster)

src/app/document/file.rs:
- Reset metadata on document change

src/app/message.rs:
- Add ToggleRightPanel and RefreshMetadata messages

src/app/model.rs:
- Add metadata: Option<DocumentMeta> field
- Add show_right_panel: bool field

src/app/update.rs:
- Handle panel toggle and metadata refresh
- Auto-refresh metadata on navigation when panel visible

src/app/view/panels.rs:
- Implement right_panel() with metadata display
- Conditional sections for basic info and EXIF data

src/app/view/canvas.rs:
- Integrate right panel into layout"
This commit is contained in:
wfx 2026-01-10 11:46:07 +01:00
parent 6623a12632
commit 823dfe9fa2
14 changed files with 616 additions and 153 deletions

269
Cargo.lock generated
View file

@ -134,9 +134,9 @@ checksum = "250f629c0161ad8107cf89319e990051fae62832fd343083bea452d93e2205fd"
[[package]] [[package]]
name = "aligned" name = "aligned"
version = "0.4.2" version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "377e4c0ba83e4431b10df45c1d4666f178ea9c552cac93e60c3a88bf32785923" checksum = "ee4508988c62edf04abd8d92897fca0c2995d907ce1dfeaf369dac3716a40685"
dependencies = [ dependencies = [
"as-slice", "as-slice",
] ]
@ -286,7 +286,7 @@ checksum = "0ae92a5119aa49cdbcf6b9f893fe4e1d98b04ccbf82ee0584ad948a44a734dea"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -343,7 +343,7 @@ dependencies = [
"wayland-backend", "wayland-backend",
"wayland-client", "wayland-client",
"wayland-protocols", "wayland-protocols",
"zbus 5.12.0", "zbus 5.13.0",
] ]
[[package]] [[package]]
@ -363,7 +363,7 @@ dependencies = [
"wayland-backend", "wayland-backend",
"wayland-client", "wayland-client",
"wayland-protocols", "wayland-protocols",
"zbus 5.12.0", "zbus 5.13.0",
] ]
[[package]] [[package]]
@ -515,7 +515,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -550,7 +550,7 @@ checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -626,7 +626,7 @@ dependencies = [
"derive_utils", "derive_utils",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -839,7 +839,7 @@ checksum = "f9abbd1bc6865053c427f7198e6af43bfdedc55ab791faed4fbd361d789575ff"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -913,9 +913,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.51" version = "1.2.52"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" checksum = "cd4932aefd12402b36c60956a4fe0035421f544799057659ff86f923657aada3"
dependencies = [ dependencies = [
"find-msvc-tools", "find-msvc-tools",
"jobserver", "jobserver",
@ -991,7 +991,7 @@ dependencies = [
"heck 0.5.0", "heck 0.5.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -1224,7 +1224,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-config" name = "cosmic-config"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"atomicwrites", "atomicwrites",
"cosmic-config-derive", "cosmic-config-derive",
@ -1239,16 +1239,16 @@ dependencies = [
"tokio", "tokio",
"tracing", "tracing",
"xdg", "xdg",
"zbus 5.12.0", "zbus 5.13.0",
] ]
[[package]] [[package]]
name = "cosmic-config-derive" name = "cosmic-config-derive"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -1282,9 +1282,9 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-settings-daemon" name = "cosmic-settings-daemon"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/pop-os/dbus-settings-bindings#70ed219735e312ac8cc3f592a01fa8023f36939b" source = "git+https://github.com/pop-os/dbus-settings-bindings#87c3c35666b926a24a1e8045fd70be2db1145e34"
dependencies = [ dependencies = [
"zbus 5.12.0", "zbus 5.13.0",
] ]
[[package]] [[package]]
@ -1313,7 +1313,7 @@ dependencies = [
[[package]] [[package]]
name = "cosmic-theme" name = "cosmic-theme"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"almost", "almost",
"cosmic-config", "cosmic-config",
@ -1445,7 +1445,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -1456,7 +1456,7 @@ checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
dependencies = [ dependencies = [
"darling_core", "darling_core",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -1494,7 +1494,7 @@ dependencies = [
"darling", "darling",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -1505,7 +1505,7 @@ checksum = "ccfae181bab5ab6c5478b2ccb69e4c68a02f8c3ec72f6616bfec9dbc599d2ee0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -1565,7 +1565,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -1678,7 +1678,7 @@ checksum = "67c78a4d8fdf9953a5c9d458f9efe940fd97a0cab0941c075a813ac594733827"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -1721,7 +1721,7 @@ checksum = "44f23cf4b44bfce11a86ace86f8a73ffdec849c9fd00a386a53d278bd9e81fb3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -1856,7 +1856,7 @@ checksum = "a0aca10fb742cb43f9e7bb8467c91aa9bcb8e3ffbc6a6f7389bb93ffc920577d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -1879,9 +1879,9 @@ dependencies = [
[[package]] [[package]]
name = "find-msvc-tools" name = "find-msvc-tools"
version = "0.1.6" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" checksum = "f449e6c6c08c865631d4890cfacf252b3d396c9bcc83adb6623cdb02a8336c41"
[[package]] [[package]]
name = "flate2" name = "flate2"
@ -2026,7 +2026,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -2138,7 +2138,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -2480,7 +2480,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"strsim", "strsim",
"syn 2.0.113", "syn 2.0.114",
"unic-langid", "unic-langid",
] ]
@ -2494,7 +2494,7 @@ dependencies = [
"i18n-config", "i18n-config",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -2524,7 +2524,7 @@ dependencies = [
[[package]] [[package]]
name = "iced" name = "iced"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"dnd", "dnd",
"iced_accessibility", "iced_accessibility",
@ -2542,7 +2542,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_accessibility" name = "iced_accessibility"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"accesskit", "accesskit",
"accesskit_winit", "accesskit_winit",
@ -2551,7 +2551,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_core" name = "iced_core"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"bytes", "bytes",
@ -2576,7 +2576,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_futures" name = "iced_futures"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"futures", "futures",
"iced_core", "iced_core",
@ -2602,7 +2602,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_graphics" name = "iced_graphics"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"bitflags 2.10.0", "bitflags 2.10.0",
"bytemuck", "bytemuck",
@ -2624,7 +2624,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_renderer" name = "iced_renderer"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"iced_graphics", "iced_graphics",
"iced_tiny_skia", "iced_tiny_skia",
@ -2636,7 +2636,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_runtime" name = "iced_runtime"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"bytes", "bytes",
"cosmic-client-toolkit", "cosmic-client-toolkit",
@ -2652,7 +2652,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_tiny_skia" name = "iced_tiny_skia"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"cosmic-text", "cosmic-text",
@ -2668,7 +2668,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_wgpu" name = "iced_wgpu"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"as-raw-xcb-connection", "as-raw-xcb-connection",
"bitflags 2.10.0", "bitflags 2.10.0",
@ -2699,7 +2699,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_widget" name = "iced_widget"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"cosmic-client-toolkit", "cosmic-client-toolkit",
"dnd", "dnd",
@ -2719,7 +2719,7 @@ dependencies = [
[[package]] [[package]]
name = "iced_winit" name = "iced_winit"
version = "0.14.0-dev" version = "0.14.0-dev"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"cosmic-client-toolkit", "cosmic-client-toolkit",
"dnd", "dnd",
@ -2911,9 +2911,9 @@ dependencies = [
[[package]] [[package]]
name = "indexmap" name = "indexmap"
version = "2.12.1" version = "2.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" checksum = "7714e70437a7dc3ac8eb7e6f8df75fd8eb422675fc7678aff7364301092b1017"
dependencies = [ dependencies = [
"equivalent", "equivalent",
"hashbrown 0.16.1", "hashbrown 0.16.1",
@ -2956,7 +2956,7 @@ checksum = "c34819042dc3d3971c46c2190835914dfbe0c3c13f61449b2997f4e9722dfa60"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -3031,9 +3031,9 @@ checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2"
[[package]] [[package]]
name = "jiff" name = "jiff"
version = "0.2.17" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a87d9b8105c23642f50cbbae03d1f75d8422c5cb98ce7ee9271f7ff7505be6b8" checksum = "e67e8da4c49d6d9909fe03361f9b620f58898859f5c7aded68351e85e71ecf50"
dependencies = [ dependencies = [
"jiff-static", "jiff-static",
"log", "log",
@ -3044,13 +3044,13 @@ dependencies = [
[[package]] [[package]]
name = "jiff-static" name = "jiff-static"
version = "0.2.17" version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b787bebb543f8969132630c51fd0afab173a86c6abae56ff3b9e5e3e3f9f6e58" checksum = "e0c84ee7f197eca9a86c6fd6cb771e55eb991632f15f2bc3ca6ec838929e6e78"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -3191,14 +3191,14 @@ checksum = "7a79a3332a6609480d7d0c9eab957bca6b455b91bb84e66d19f5ff66294b85b8"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.179" version = "0.2.180"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c5a2d376baa530d1238d133232d15e239abad80d05838b4b59354e5268af431f" checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc"
[[package]] [[package]]
name = "libcosmic" name = "libcosmic"
version = "0.1.0" version = "0.1.0"
source = "git+https://github.com/pop-os/libcosmic.git#a9f64c33ce9159485be5dad1ce07ccf7c12399d5" source = "git+https://github.com/pop-os/libcosmic.git#b9c24d24212a865977db4871efc13ff890055648"
dependencies = [ dependencies = [
"apply", "apply",
"ashpd 0.12.0", "ashpd 0.12.0",
@ -3240,7 +3240,7 @@ dependencies = [
"tracing", "tracing",
"unicode-segmentation", "unicode-segmentation",
"url", "url",
"zbus 5.12.0", "zbus 5.13.0",
] ]
[[package]] [[package]]
@ -3604,19 +3604,6 @@ dependencies = [
"memoffset 0.7.1", "memoffset 0.7.1",
] ]
[[package]]
name = "nix"
version = "0.30.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6"
dependencies = [
"bitflags 2.10.0",
"cfg-if",
"cfg_aliases 0.2.1",
"libc",
"memoffset 0.9.1",
]
[[package]] [[package]]
name = "noctua" name = "noctua"
version = "0.1.0" version = "0.1.0"
@ -3628,6 +3615,7 @@ dependencies = [
"i18n-embed", "i18n-embed",
"i18n-embed-fl", "i18n-embed-fl",
"image", "image",
"kamadak-exif",
"libcosmic", "libcosmic",
"log", "log",
"open", "open",
@ -3699,7 +3687,7 @@ checksum = "ed3955f1a9c7c0c15e092f9c887db08b1fc683305fdf6eb6684f22555355e202"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -3761,7 +3749,7 @@ dependencies = [
"proc-macro-crate 3.4.0", "proc-macro-crate 3.4.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -4118,7 +4106,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -4152,7 +4140,7 @@ dependencies = [
"by_address", "by_address",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -4284,7 +4272,7 @@ dependencies = [
"phf_shared 0.11.3", "phf_shared 0.11.3",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -4297,7 +4285,7 @@ dependencies = [
"phf_shared 0.13.1", "phf_shared 0.13.1",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -4341,7 +4329,7 @@ checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -4518,14 +4506,14 @@ dependencies = [
"proc-macro-error-attr2", "proc-macro-error-attr2",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.104" version = "1.0.105"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" checksum = "535d180e0ecab6268a3e718bb9fd44db66bbbc256257165fc699dadf70d16fe7"
dependencies = [ dependencies = [
"unicode-ident", "unicode-ident",
] ]
@ -4538,7 +4526,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
"version_check", "version_check",
"yansi", "yansi",
] ]
@ -4559,7 +4547,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b" checksum = "52717f9a02b6965224f95ca2a81e2e0c5c43baacd28ca057577988930b6c3d5b"
dependencies = [ dependencies = [
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -4597,9 +4585,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.42" version = "1.0.43"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" checksum = "dc74d9a594b72ae6656596548f56f667211f8a97b3d4c3d467150794690dc40a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -4939,7 +4927,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"rust-embed-utils", "rust-embed-utils",
"syn 2.0.113", "syn 2.0.114",
"walkdir", "walkdir",
] ]
@ -5094,14 +5082,14 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.148" version = "1.0.149"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3084b546a1dd6289475996f182a22aba973866ea8e8b02c51d9f46b1336a22da" checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"itoa", "itoa",
@ -5119,7 +5107,7 @@ checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -5444,9 +5432,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.113" version = "2.0.114"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "678faa00651c9eb72dd2020cbdf275d92eccb2400d568e419efdd64838145cb4" checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -5461,7 +5449,7 @@ checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -5533,7 +5521,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -5544,7 +5532,7 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -5685,7 +5673,7 @@ checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -5775,7 +5763,7 @@ checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -5915,14 +5903,15 @@ checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.7" version = "2.5.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b" checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
dependencies = [ dependencies = [
"form_urlencoded", "form_urlencoded",
"idna", "idna",
"percent-encoding", "percent-encoding",
"serde", "serde",
"serde_derive",
] ]
[[package]] [[package]]
@ -6074,7 +6063,7 @@ dependencies = [
"bumpalo", "bumpalo",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -6498,7 +6487,7 @@ checksum = "942ac266be9249c84ca862f0a164a39533dc2f6f33dc98ec89c8da99b82ea0bd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -6509,7 +6498,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -6520,7 +6509,7 @@ checksum = "da33557140a288fae4e1d5f8873aaf9eb6613a9cf82c3e070223ff177f598b60"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -6531,7 +6520,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -7093,7 +7082,7 @@ checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
"synstructure", "synstructure",
] ]
@ -7115,7 +7104,7 @@ dependencies = [
"futures-sink", "futures-sink",
"futures-util", "futures-util",
"hex", "hex",
"nix 0.26.4", "nix",
"once_cell", "once_cell",
"ordered-stream", "ordered-stream",
"rand 0.8.5", "rand 0.8.5",
@ -7135,9 +7124,9 @@ dependencies = [
[[package]] [[package]]
name = "zbus" name = "zbus"
version = "5.12.0" version = "5.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b622b18155f7a93d1cd2dc8c01d2d6a44e08fb9ebb7b3f9e6ed101488bad6c91" checksum = "7515214ab069b46f614dee52c1256015cdc1a0b441ed612118e2871014956741"
dependencies = [ dependencies = [
"async-broadcast 0.7.2", "async-broadcast 0.7.2",
"async-executor", "async-executor",
@ -7153,8 +7142,9 @@ dependencies = [
"futures-core", "futures-core",
"futures-lite 2.6.1", "futures-lite 2.6.1",
"hex", "hex",
"nix 0.30.1", "libc",
"ordered-stream", "ordered-stream",
"rustix 1.1.3",
"serde", "serde",
"serde_repr", "serde_repr",
"tokio", "tokio",
@ -7163,9 +7153,9 @@ dependencies = [
"uuid", "uuid",
"windows-sys 0.61.2", "windows-sys 0.61.2",
"winnow 0.7.14", "winnow 0.7.14",
"zbus_macros 5.12.0", "zbus_macros 5.13.0",
"zbus_names 4.2.0", "zbus_names 4.3.1",
"zvariant 5.8.0", "zvariant 5.9.0",
] ]
[[package]] [[package]]
@ -7184,17 +7174,17 @@ dependencies = [
[[package]] [[package]]
name = "zbus_macros" name = "zbus_macros"
version = "5.12.0" version = "5.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cdb94821ca8a87ca9c298b5d1cbd80e2a8b67115d99f6e4551ac49e42b6a314" checksum = "04f54d8a5b4e9c46cf4a9732da4899b12851b5df952fc8deda23aca1d6f3e26c"
dependencies = [ dependencies = [
"proc-macro-crate 3.4.0", "proc-macro-crate 3.4.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
"zbus_names 4.2.0", "zbus_names 4.3.1",
"zvariant 5.8.0", "zvariant 5.9.0",
"zvariant_utils 3.2.1", "zvariant_utils 3.3.0",
] ]
[[package]] [[package]]
@ -7210,14 +7200,13 @@ dependencies = [
[[package]] [[package]]
name = "zbus_names" name = "zbus_names"
version = "4.2.0" version = "4.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7be68e64bf6ce8db94f63e72f0c7eb9a60d733f7e0499e628dfab0f84d6bcb97" checksum = "ffd8af6d5b78619bab301ff3c560a5bd22426150253db278f164d6cf3b72c50f"
dependencies = [ dependencies = [
"serde", "serde",
"static_assertions",
"winnow 0.7.14", "winnow 0.7.14",
"zvariant 5.8.0", "zvariant 5.9.0",
] ]
[[package]] [[package]]
@ -7228,22 +7217,22 @@ checksum = "6df3dc4292935e51816d896edcd52aa30bc297907c26167fec31e2b0c6a32524"
[[package]] [[package]]
name = "zerocopy" name = "zerocopy"
version = "0.8.31" version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd74ec98b9250adb3ca554bdde269adf631549f51d8a8f8f0a10b50f1cb298c3" checksum = "668f5168d10b9ee831de31933dc111a459c97ec93225beb307aed970d1372dfd"
dependencies = [ dependencies = [
"zerocopy-derive", "zerocopy-derive",
] ]
[[package]] [[package]]
name = "zerocopy-derive" name = "zerocopy-derive"
version = "0.8.31" version = "0.8.33"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d8a8d209fdf45cf5138cbb5a506f6b52522a25afccc534d1475dad8e31105c6a" checksum = "2c7962b26b0a8685668b671ee4b54d007a67d4eaf05fda79ac0ecf41e32270f1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
@ -7263,7 +7252,7 @@ checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
"synstructure", "synstructure",
] ]
@ -7298,14 +7287,14 @@ checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
] ]
[[package]] [[package]]
name = "zmij" name = "zmij"
version = "1.0.11" version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcb2c125bd7365735bebeb420ccb880265ed2d2bddcbcd49f597fdfe6bd5e577" checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8"
[[package]] [[package]]
name = "zune-core" name = "zune-core"
@ -7362,17 +7351,17 @@ dependencies = [
[[package]] [[package]]
name = "zvariant" name = "zvariant"
version = "5.8.0" version = "5.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c" checksum = "788ca131e3757991e4b9fe9f7b78ae302749ed96093ff60858a1f4732b04b164"
dependencies = [ dependencies = [
"endi", "endi",
"enumflags2", "enumflags2",
"serde", "serde",
"url", "url",
"winnow 0.7.14", "winnow 0.7.14",
"zvariant_derive 5.8.0", "zvariant_derive 5.9.0",
"zvariant_utils 3.2.1", "zvariant_utils 3.3.0",
] ]
[[package]] [[package]]
@ -7390,15 +7379,15 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_derive" name = "zvariant_derive"
version = "5.8.0" version = "5.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006" checksum = "0e69a2f6b221a6fec9bd6bcc77c19360cca106f92a5fd948b8aa17d2339c7505"
dependencies = [ dependencies = [
"proc-macro-crate 3.4.0", "proc-macro-crate 3.4.0",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.113", "syn 2.0.114",
"zvariant_utils 3.2.1", "zvariant_utils 3.3.0",
] ]
[[package]] [[package]]
@ -7414,13 +7403,13 @@ dependencies = [
[[package]] [[package]]
name = "zvariant_utils" name = "zvariant_utils"
version = "3.2.1" version = "3.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6949d142f89f6916deca2232cf26a8afacf2b9fdc35ce766105e104478be599" checksum = "f75c23a64ef8f40f13a6989991e643554d9bef1d682a281160cf0c1bc389c5e9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"serde", "serde",
"syn 2.0.113", "syn 2.0.114",
"winnow 0.7.14", "winnow 0.7.14",
] ]

View file

@ -18,6 +18,8 @@ categories = ["gui", "multimedia::graphics", "multimedia::images"]
# Error handling # Error handling
anyhow = "1" anyhow = "1"
kamadak-exif = "0.5.5"
# Async / concurrency # Async / concurrency
futures-util = "0.3.31" futures-util = "0.3.31"
tokio = { version = "1.48.0", features = ["full"] } tokio = { version = "1.48.0", features = ["full"] }

View file

@ -37,3 +37,25 @@ scale = Scale
## Error messages ## Error messages
error-failed-to-open = Failed to open “{ $path }”. error-failed-to-open = Failed to open “{ $path }”.
error-unsupported-format = Unsupported file format. error-unsupported-format = Unsupported file format.
# Metadata panel
metadata = Metadata
file-name = File
format = Format
resolution = Resolution
file-size = Size
color-type = Color
# EXIF data
exif-data = EXIF Data
camera = Camera
date-taken = Date
exposure = Exposure
aperture = Aperture
iso = ISO
focal-length = Focal
gps = GPS
# States
loading-metadata = Loading...
no-document = No document

View file

@ -91,6 +91,8 @@ fn load_document_into_model(model: &mut AppModel, path: &Path) {
match open_document(path.to_path_buf()) { match open_document(path.to_path_buf()) {
Ok(doc) => { Ok(doc) => {
model.document = Some(doc); model.document = Some(doc);
// Reset cached metadata so it gets reloaded when panel is visible.
model.metadata = None;
model.current_path = Some(path.to_path_buf()); model.current_path = Some(path.to_path_buf());
model.clear_error(); model.clear_error();
@ -182,3 +184,17 @@ pub fn navigate_prev(model: &mut AppModel) {
load_document_into_model(model, &path); load_document_into_model(model, &path);
} }
} }
// ---------------------------------------------------------------------------
// File metadata helpers
// ---------------------------------------------------------------------------
/// Retrieve the file size in bytes. Returns 0 if the file cannot be accessed.
pub fn file_size(path: &Path) -> u64 {
fs::metadata(path).map(|m| m.len()).unwrap_or(0)
}
/// Read raw bytes from a file for metadata extraction (e.g., EXIF).
/// Returns None if the file cannot be read.
pub fn read_file_bytes(path: &Path) -> Option<Vec<u8>> {
fs::read(path).ok()
}

View file

@ -1,2 +1,267 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// src/app/document/meta.rs // src/app/document/meta.rs
//
// Document metadata extraction (basic info and EXIF).
use std::io::Cursor;
use std::path::Path;
use image::DynamicImage;
use exif::{In, Reader as ExifReader, Tag, Value};
use super::file;
/// Basic document metadata (always available).
#[derive(Debug, Clone)]
pub struct BasicMeta {
/// File name (without path).
pub file_name: String,
/// Full file path.
pub file_path: String,
/// Image format as string (e.g., "PNG", "JPEG", "PDF").
pub format: String,
/// Width in pixels.
pub width: u32,
/// Height in pixels.
pub height: u32,
/// File size in bytes.
pub file_size: u64,
/// Color type description (e.g., "RGBA8", "RGB8", "Grayscale").
pub color_type: String,
}
impl BasicMeta {
/// Format file size as human-readable string.
pub fn file_size_display(&self) -> String {
const KB: u64 = 1024;
const MB: u64 = KB * 1024;
const GB: u64 = MB * 1024;
if self.file_size >= GB {
format!("{:.2} GB", self.file_size as f64 / GB as f64)
} else if self.file_size >= MB {
format!("{:.2} MB", self.file_size as f64 / MB as f64)
} else if self.file_size >= KB {
format!("{:.1} KB", self.file_size as f64 / KB as f64)
} else {
format!("{} B", self.file_size)
}
}
/// Format resolution as "W × H".
pub fn resolution_display(&self) -> String {
format!("{} × {}", self.width, self.height)
}
}
/// EXIF metadata (optional, mainly for JPEG/TIFF).
#[derive(Debug, Clone, Default)]
pub struct ExifMeta {
pub camera_make: Option<String>,
pub camera_model: Option<String>,
pub date_time: Option<String>,
pub exposure_time: Option<String>,
pub f_number: Option<String>,
pub iso: Option<u32>,
pub focal_length: Option<String>,
pub gps_latitude: Option<f64>,
pub gps_longitude: Option<f64>,
}
impl ExifMeta {
/// Combined camera make and model for display.
pub fn camera_display(&self) -> Option<String> {
match (&self.camera_make, &self.camera_model) {
(Some(make), Some(model)) => {
if model.starts_with(make) {
Some(model.clone())
} else {
Some(format!("{} {}", make, model))
}
}
(Some(make), None) => Some(make.clone()),
(None, Some(model)) => Some(model.clone()),
(None, None) => None,
}
}
/// Format GPS coordinates for display.
pub fn gps_display(&self) -> Option<String> {
match (self.gps_latitude, self.gps_longitude) {
(Some(lat), Some(lon)) => Some(format!("{:.5}, {:.5}", lat, lon)),
_ => None,
}
}
}
/// Complete document metadata container.
#[derive(Debug, Clone)]
pub struct DocumentMeta {
pub basic: BasicMeta,
pub exif: Option<ExifMeta>,
}
// ---------------------------------------------------------------------------
// Extraction functions
// ---------------------------------------------------------------------------
/// Extract basic metadata common to all document types.
fn extract_basic_meta(
path: &Path,
width: u32,
height: u32,
format: &str,
color_type: String,
) -> BasicMeta {
let file_name = path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("unknown")
.to_string();
let file_path = path.to_string_lossy().to_string();
let file_size = file::file_size(path);
BasicMeta {
file_name,
file_path,
format: format.to_string(),
width,
height,
file_size,
color_type,
}
}
/// Extract EXIF metadata from file bytes.
fn extract_exif_from_bytes(data: &[u8]) -> Option<ExifMeta> {
let mut cursor = Cursor::new(data);
let exif = ExifReader::new().read_from_container(&mut cursor).ok()?;
let mut meta = ExifMeta::default();
// Camera info.
if let Some(field) = exif.get_field(Tag::Make, In::PRIMARY) {
meta.camera_make = field.display_value().to_string().into();
}
if let Some(field) = exif.get_field(Tag::Model, In::PRIMARY) {
meta.camera_model = field.display_value().to_string().into();
}
// Date/time.
if let Some(field) = exif.get_field(Tag::DateTimeOriginal, In::PRIMARY) {
meta.date_time = Some(field.display_value().to_string());
} else if let Some(field) = exif.get_field(Tag::DateTime, In::PRIMARY) {
meta.date_time = Some(field.display_value().to_string());
}
// Exposure settings.
if let Some(field) = exif.get_field(Tag::ExposureTime, In::PRIMARY) {
meta.exposure_time = Some(field.display_value().to_string());
}
if let Some(field) = exif.get_field(Tag::FNumber, In::PRIMARY) {
meta.f_number = Some(format!("f/{}", field.display_value()));
}
if let Some(field) = exif.get_field(Tag::PhotographicSensitivity, In::PRIMARY) {
if let Value::Short(ref vals) = field.value {
if let Some(&iso) = vals.first() {
meta.iso = Some(iso as u32);
}
}
}
if let Some(field) = exif.get_field(Tag::FocalLength, In::PRIMARY) {
meta.focal_length = Some(field.display_value().to_string());
}
// GPS coordinates.
meta.gps_latitude = extract_gps_coord(&exif, Tag::GPSLatitude, Tag::GPSLatitudeRef);
meta.gps_longitude = extract_gps_coord(&exif, Tag::GPSLongitude, Tag::GPSLongitudeRef);
Some(meta)
}
/// Extract a GPS coordinate (latitude or longitude) from EXIF data.
fn extract_gps_coord(exif: &exif::Exif, coord_tag: Tag, ref_tag: Tag) -> Option<f64> {
let field = exif.get_field(coord_tag, In::PRIMARY)?;
let degrees = match &field.value {
Value::Rational(rats) if rats.len() >= 3 => {
let d = rats[0].to_f64();
let m = rats[1].to_f64();
let s = rats[2].to_f64();
d + m / 60.0 + s / 3600.0
}
_ => return None,
};
// Check reference (N/S or E/W) for sign.
let sign = if let Some(ref_field) = exif.get_field(ref_tag, In::PRIMARY) {
let ref_str = ref_field.display_value().to_string();
if ref_str.contains('S') || ref_str.contains('W') {
-1.0
} else {
1.0
}
} else {
1.0
};
Some(degrees * sign)
}
/// Determine color type string from DynamicImage.
fn color_type_string(img: &DynamicImage) -> String {
use image::DynamicImage::*;
match img {
ImageLuma8(_) => "Grayscale 8-bit".to_string(),
ImageLumaA8(_) => "Grayscale+Alpha 8-bit".to_string(),
ImageRgb8(_) => "RGB 8-bit".to_string(),
ImageRgba8(_) => "RGBA 8-bit".to_string(),
ImageLuma16(_) => "Grayscale 16-bit".to_string(),
ImageLumaA16(_) => "Grayscale+Alpha 16-bit".to_string(),
ImageRgb16(_) => "RGB 16-bit".to_string(),
ImageRgba16(_) => "RGBA 16-bit".to_string(),
ImageRgb32F(_) => "RGB 32-bit float".to_string(),
ImageRgba32F(_) => "RGBA 32-bit float".to_string(),
_ => "Unknown".to_string(),
}
}
/// Determine format string from file extension.
fn format_from_extension(path: &Path) -> String {
path.extension()
.and_then(|e| e.to_str())
.map(|e| e.to_uppercase())
.unwrap_or_else(|| "Unknown".to_string())
}
// ---------------------------------------------------------------------------
// Public builder functions for each document type
// ---------------------------------------------------------------------------
/// Build metadata for a raster document.
pub fn build_raster_meta(path: &Path, img: &DynamicImage, width: u32, height: u32) -> DocumentMeta {
let format = format_from_extension(path);
let color_type = color_type_string(img);
let basic = extract_basic_meta(path, width, height, &format, color_type);
// Try to extract EXIF (mainly for JPEG/TIFF).
let exif = file::read_file_bytes(path).and_then(|bytes| extract_exif_from_bytes(&bytes));
DocumentMeta { basic, exif }
}
/// Build metadata for a vector document.
pub fn build_vector_meta(path: &Path, width: u32, height: u32) -> DocumentMeta {
let basic = extract_basic_meta(path, width, height, "SVG", "Vector".to_string());
DocumentMeta { basic, exif: None }
}
/// Build metadata for a portable document.
pub fn build_portable_meta(path: &Path, width: u32, height: u32, page_count: u32) -> DocumentMeta {
let format = format!("PDF ({} pages)", page_count);
let basic = extract_basic_meta(path, width, height, &format, "Rendered".to_string());
DocumentMeta { basic, exif: None }
}

View file

@ -100,4 +100,13 @@ impl DocumentContent {
DocumentContent::Portable(doc) => doc.dimensions(), DocumentContent::Portable(doc) => doc.dimensions(),
} }
} }
/// Extract metadata from the document.
/// This may involve file I/O for EXIF data, so call lazily.
pub fn extract_meta(&self) -> meta::DocumentMeta {
match self {
DocumentContent::Raster(doc) => doc.extract_meta(),
DocumentContent::Vector(doc) => doc.extract_meta(),
DocumentContent::Portable(doc) => doc.extract_meta(),
}
}
} }

View file

@ -62,4 +62,10 @@ impl PortableDocument {
// self.rendered = render_page_to_dynamic(...); // self.rendered = render_page_to_dynamic(...);
// self.refresh_handle(); // self.refresh_handle();
} }
/// Extract metadata for this portable document.
pub fn extract_meta(&self) -> super::meta::DocumentMeta {
let (width, height) = self.dimensions();
super::meta::build_portable_meta(&self.path, width, height, self.page_count)
}
} }

View file

@ -62,4 +62,11 @@ impl RasterDocument {
)) ))
} }
} }
/// Extract metadata for this raster document.
pub fn extract_meta(&self) -> super::meta::DocumentMeta {
let path = self.path.as_deref().unwrap_or(std::path::Path::new(""));
let (width, height) = self.dimensions();
super::meta::build_raster_meta(path, &self.image, width, height)
}
} }

View file

@ -45,4 +45,10 @@ impl VectorDocument {
// TODO: re-render SVG to DynamicImage and rebuild handle. // TODO: re-render SVG to DynamicImage and rebuild handle.
// Update self.width and self.height accordingly. // Update self.width and self.height accordingly.
} }
/// Extract metadata for this vector document.
pub fn extract_meta(&self) -> super::meta::DocumentMeta {
let (width, height) = self.dimensions();
super::meta::build_vector_meta(&self.path, width, height)
}
} }

View file

@ -20,6 +20,9 @@ pub enum AppMessage {
NextDocument, NextDocument,
PrevDocument, PrevDocument,
/// Refresh metadata (e.g., when panel becomes visible or document changes).
RefreshMetadata,
/// Basic view / panel toggles. /// Basic view / panel toggles.
ToggleLeftPanel, ToggleLeftPanel,
ToggleRightPanel, ToggleRightPanel,

View file

@ -6,6 +6,8 @@
use std::path::PathBuf; use std::path::PathBuf;
use crate::app::document::DocumentContent; use crate::app::document::DocumentContent;
use crate::app::document::meta::DocumentMeta;
use crate::config::AppConfig; use crate::config::AppConfig;
/// How the document is currently fitted into the window. /// How the document is currently fitted into the window.
@ -51,6 +53,10 @@ pub struct AppModel {
/// Currently opened document (raster/vector/portable). /// Currently opened document (raster/vector/portable).
pub document: Option<DocumentContent>, pub document: Option<DocumentContent>,
/// Cached metadata for the current document.
/// Loaded lazily when the right panel is opened.
pub metadata: Option<DocumentMeta>,
/// Path of the currently opened document, if any. /// Path of the currently opened document, if any.
pub current_path: Option<PathBuf>, pub current_path: Option<PathBuf>,
@ -84,6 +90,7 @@ impl AppModel {
Self { Self {
config, config,
document: None, document: None,
metadata: None,
current_path: None, current_path: None,
folder_entries: Vec::new(), folder_entries: Vec::new(),
current_index: None, current_index: None,

View file

@ -17,14 +17,26 @@ pub fn update(model: &mut AppModel, msg: AppMessage) {
// ===== File / navigation ========================================================== // ===== File / navigation ==========================================================
AppMessage::OpenPath(path) => { AppMessage::OpenPath(path) => {
document::file::open_single_file(model, &path); document::file::open_single_file(model, &path);
// Refresh metadata if panel is visible.
if model.show_right_panel {
refresh_metadata(model);
}
} }
AppMessage::NextDocument => { AppMessage::NextDocument => {
document::file::navigate_next(model); document::file::navigate_next(model);
// Refresh metadata if panel is visible.
if model.show_right_panel {
refresh_metadata(model);
}
} }
AppMessage::PrevDocument => { AppMessage::PrevDocument => {
document::file::navigate_prev(model); document::file::navigate_prev(model);
// Refresh metadata if panel is visible.
if model.show_right_panel {
refresh_metadata(model);
}
} }
// ===== Panels ===================================================================== // ===== Panels =====================================================================
@ -33,6 +45,10 @@ pub fn update(model: &mut AppModel, msg: AppMessage) {
} }
AppMessage::ToggleRightPanel => { AppMessage::ToggleRightPanel => {
model.show_right_panel = !model.show_right_panel; model.show_right_panel = !model.show_right_panel;
// Load metadata lazily when panel becomes visible.
if model.show_right_panel && model.metadata.is_none() {
refresh_metadata(model);
}
} }
// ===== View / zoom =============================================================== // ===== View / zoom ===============================================================
@ -102,6 +118,11 @@ pub fn update(model: &mut AppModel, msg: AppMessage) {
} }
} }
// ===== Metadata ==================================================================
AppMessage::RefreshMetadata => {
refresh_metadata(model);
}
// ===== Error handling ============================================================ // ===== Error handling ============================================================
AppMessage::ShowError(msg) => { AppMessage::ShowError(msg) => {
model.set_error(msg); model.set_error(msg);
@ -139,3 +160,8 @@ fn current_zoom(model: &AppModel) -> f32 {
ViewMode::Custom(z) => z, ViewMode::Custom(z) => z,
} }
} }
/// Refresh metadata from the current document.
fn refresh_metadata(model: &mut AppModel) {
model.metadata = model.document.as_ref().map(|doc| doc.extract_meta());
}

View file

@ -1,8 +1,8 @@
// SPDX-License-Identifier: GPL-3.0-or-later // SPDX-License-Identifier: GPL-3.0-or-later
// src/app/view/canvas.rs // src/app/view/canvas.rs
// //
// Center canvas for displaying the current document. /// Renders the center canvas area with the current document.
//
use cosmic::iced::{Alignment, Length}; use cosmic::iced::{Alignment, Length};
use cosmic::widget::{container, image, text, Column, Row}; use cosmic::widget::{container, image, text, Column, Row};
use cosmic::Element; use cosmic::Element;

View file

@ -12,8 +12,55 @@ use crate::app::model::ViewMode;
use crate::app::{AppMessage, AppModel}; use crate::app::{AppMessage, AppModel};
/// Top header bar (global actions, toggles). /// Top header bar (global actions, toggles).
pub fn header(_model: &AppModel) -> Element<'_, AppMessage> { pub fn header(model: &AppModel) -> Element<'_, AppMessage> {
let content = Row::new().spacing(8).align_y(Alignment::Center); // Left panel toggle button.
let left_toggle = widget::button::icon(widget::icon::from_name(if model.show_left_panel {
"sidebar-show-left-symbolic"
} else {
"sidebar-show-left-symbolic"
}))
.on_press(AppMessage::ToggleLeftPanel);
// Right panel toggle button.
let right_toggle = widget::button::icon(widget::icon::from_name(if model.show_right_panel {
"sidebar-show-right-symbolic"
} else {
"sidebar-show-right-symbolic"
}))
.on_press(AppMessage::ToggleRightPanel);
// File name display (centered).
let file_name = model
.current_path
.as_ref()
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.unwrap_or("");
let title = Text::new(file_name);
// Spacer to push title to center and right_toggle to the right.
let left_section = Row::new()
.spacing(8)
.align_y(Alignment::Center)
.push(left_toggle);
let center_section = Container::new(title)
.width(Length::Fill)
.align_x(Alignment::Center);
let right_section = Row::new()
.spacing(8)
.align_y(Alignment::Center)
.push(right_toggle);
let content = Row::new()
.spacing(8)
.align_y(Alignment::Center)
.width(Length::Fill)
.push(left_section)
.push(center_section)
.push(right_section);
Container::new(content) Container::new(content)
.width(Length::Fill) .width(Length::Fill)
@ -75,18 +122,76 @@ pub fn right_panel(model: &AppModel) -> Option<Element<'_, AppMessage>> {
return None; return None;
} }
let meta = Column::new() let mut content = Column::new().spacing(8).padding(4);
.spacing(4)
.push(Text::new("Metadata"))
.push(Text::new(format!(
"Current index: {:?}",
model.current_index
)));
let panel = Container::new(meta) // Section header.
.width(Length::Fixed(220.0)) content = content.push(Text::new(fl!("metadata")).size(16).width(Length::Fill));
content = content.push(widget::divider::horizontal::default());
if let Some(meta) = &model.metadata {
// Basic information section.
content = content
.push(meta_row(fl!("file-name"), meta.basic.file_name.clone()))
.push(meta_row(fl!("format"), meta.basic.format.clone()))
.push(meta_row(fl!("resolution"), meta.basic.resolution_display()))
.push(meta_row(fl!("file-size"), meta.basic.file_size_display()))
.push(meta_row(fl!("color-type"), meta.basic.color_type.clone()));
// EXIF section (if available).
if let Some(exif) = &meta.exif {
content = content
.push(widget::vertical_space().height(Length::Fixed(12.0)))
.push(Text::new(fl!("exif-data")).size(14))
.push(widget::divider::horizontal::default());
if let Some(camera) = exif.camera_display() {
content = content.push(meta_row(fl!("camera"), camera));
}
if let Some(date) = &exif.date_time {
content = content.push(meta_row(fl!("date-taken"), date.clone()));
}
if let Some(exp) = &exif.exposure_time {
content = content.push(meta_row(fl!("exposure"), exp.clone()));
}
if let Some(aperture) = &exif.f_number {
content = content.push(meta_row(fl!("aperture"), aperture.clone()));
}
if let Some(iso) = exif.iso {
content = content.push(meta_row(fl!("iso"), iso.to_string()));
}
if let Some(focal) = &exif.focal_length {
content = content.push(meta_row(fl!("focal-length"), focal.clone()));
}
if let Some(gps) = exif.gps_display() {
content = content.push(meta_row(fl!("gps"), gps));
}
}
} else if model.document.is_some() {
// Document exists but metadata not yet loaded.
content = content.push(Text::new(fl!("loading-metadata")));
} else {
// No document loaded.
content = content.push(Text::new(fl!("no-document")));
}
let panel = Container::new(widget::scrollable(content).height(Length::Fill))
.width(Length::Fixed(240.0))
.height(Length::Fill) .height(Length::Fill)
.padding(8); .padding(8);
Some(panel.into()) Some(panel.into())
} }
/// Helper to create a label-value row for metadata display.
fn meta_row(label: String, value: String) -> Element<'static, AppMessage> {
Row::new()
.spacing(8)
.push(
Text::new(format!("{}:", label))
.size(12)
.width(Length::Fixed(80.0)),
)
.push(Text::new(value).size(12).width(Length::Fill))
.into()
}