feat(clipboard): pass original JPEG/GIF through verbatim beside the PNG floor
android / android (push) Has been cancelled
audit / bun-audit (push) Successful in 14s
audit / cargo-audit (push) Successful in 2m29s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 54s
decky / build-publish (push) Successful in 30s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 18s
apple / swift (push) Successful in 1m23s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 13s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
ci / bench (push) Successful in 5m58s
ci / rust (push) Failing after 6m54s
release / apple (push) Successful in 5m52s
arch / build-publish (push) Failing after 11m7s
flatpak / build-publish (push) Failing after 8m7s
deb / build-publish (push) Successful in 13m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m31s
docker / deploy-docs (push) Successful in 24s
apple / screenshots (push) Successful in 6m28s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m5s
windows-host / package (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
android / android (push) Has been cancelled
audit / bun-audit (push) Successful in 14s
audit / cargo-audit (push) Successful in 2m29s
ci / web (push) Successful in 50s
ci / docs-site (push) Successful in 54s
decky / build-publish (push) Successful in 30s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 18s
apple / swift (push) Successful in 1m23s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 12s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 13s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 12s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 11s
ci / bench (push) Successful in 5m58s
ci / rust (push) Failing after 6m54s
release / apple (push) Successful in 5m52s
arch / build-publish (push) Failing after 11m7s
flatpak / build-publish (push) Failing after 8m7s
deb / build-publish (push) Successful in 13m52s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m31s
docker / deploy-docs (push) Successful in 24s
apple / screenshots (push) Successful in 6m28s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 18m5s
windows-host / package (push) Has been cancelled
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Has been cancelled
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Has been cancelled
windows / build (aarch64-pc-windows-msvc) (push) Has been cancelled
windows / build (x86_64-pc-windows-msvc) (push) Has been cancelled
Transcoding every image to PNG was the Phase-1 floor, but it bloats lossy originals (a copied JPEG re-encoded lossless is 5-10x the bytes for zero quality gain — feeding the render-timeout on constrained links) and flattens GIFs to one frame. The wire already carries kind LISTS, so offer the original format beside the portable fallback and let the destination pick: - New wire kinds image/jpeg + image/gif (§3.5 extension; strings only, no protocol change). image/png stays the universal floor every peer accepts. - macOS: public.jpeg / com.compuserve.gif rows pass through verbatim both directions; the PNG floor is announced whenever any image is present. - Windows: registered JFIF/GIF formats read + promised verbatim; every image offer still promises CF_DIB, whose delayed render now fetches the richest offered kind (png > jpeg > gif) through the generalized image_to_dib. - Linux maps gain the matching rows (native MIME pass-through). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Generated
+99
@@ -560,6 +560,12 @@ dependencies = [
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "byteorder-lite"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f1fe948ff07f4bd06c30984e69f5b4899c516a3ef74f34df92a2df2ab535495"
|
||||
|
||||
[[package]]
|
||||
name = "bytes"
|
||||
version = "1.12.0"
|
||||
@@ -756,6 +762,12 @@ dependencies = [
|
||||
"cc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "color_quant"
|
||||
version = "1.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b"
|
||||
|
||||
[[package]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.5"
|
||||
@@ -1141,6 +1153,15 @@ dependencies = [
|
||||
"getrandom 0.3.4",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fdeflate"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1e6853b52649d4ac5c0bd02320cddc5ba956bdb407c4b75a2c6b75bf51500f8c"
|
||||
dependencies = [
|
||||
"simd-adler32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fec-rs"
|
||||
version = "0.1.0"
|
||||
@@ -1495,6 +1516,16 @@ dependencies = [
|
||||
"polyval",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gif"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ee8cfcc411d9adbbaba82fb72661cc1bcca13e8bba98b364e62b2dba8f960159"
|
||||
dependencies = [
|
||||
"color_quant",
|
||||
"weezl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "gio"
|
||||
version = "0.22.6"
|
||||
@@ -1988,6 +2019,23 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "image"
|
||||
version = "0.25.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
|
||||
dependencies = [
|
||||
"bytemuck",
|
||||
"byteorder-lite",
|
||||
"color_quant",
|
||||
"gif",
|
||||
"moxcms",
|
||||
"num-traits",
|
||||
"png",
|
||||
"zune-core",
|
||||
"zune-jpeg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "indexmap"
|
||||
version = "2.14.0"
|
||||
@@ -2386,6 +2434,16 @@ dependencies = [
|
||||
"windows-sys 0.61.2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "moxcms"
|
||||
version = "0.8.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bb85c154ba489f01b25c0d36ae69a87e4a1c73a72631fc6c0eb6dde34a73e44b"
|
||||
dependencies = [
|
||||
"num-traits",
|
||||
"pxfm",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nasm-rs"
|
||||
version = "0.3.2"
|
||||
@@ -2805,6 +2863,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"ashpd",
|
||||
"futures-util",
|
||||
"image",
|
||||
"libc",
|
||||
"punktfunk-core",
|
||||
"quinn",
|
||||
@@ -3075,6 +3134,19 @@ version = "0.3.33"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crc32fast",
|
||||
"fdeflate",
|
||||
"flate2",
|
||||
"miniz_oxide",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "polling"
|
||||
version = "3.11.0"
|
||||
@@ -3382,6 +3454,12 @@ dependencies = [
|
||||
"winresource",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pxfm"
|
||||
version = "0.1.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
|
||||
|
||||
[[package]]
|
||||
name = "pyrowave-sys"
|
||||
version = "0.13.0"
|
||||
@@ -5202,6 +5280,12 @@ dependencies = [
|
||||
"rustls-pki-types",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "weezl"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a28ac98ddc8b9274cb41bb4d9d4d5c425b6020c50c46f25559911905610b4a88"
|
||||
|
||||
[[package]]
|
||||
name = "wide"
|
||||
version = "0.7.33"
|
||||
@@ -6014,6 +6098,21 @@ version = "1.0.21"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
|
||||
|
||||
[[package]]
|
||||
name = "zune-core"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cb8a0807f7c01457d0379ba880ba6322660448ddebc890ce29bb64da71fb40f9"
|
||||
|
||||
[[package]]
|
||||
name = "zune-jpeg"
|
||||
version = "0.5.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "27bc9d5b815bc103f142aa054f561d9187d191692ec7c2d1e2b4737f8dbd7296"
|
||||
dependencies = [
|
||||
"zune-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zvariant"
|
||||
version = "5.12.0"
|
||||
|
||||
@@ -29,6 +29,11 @@ public final class ClipboardSync: NSObject {
|
||||
("text/rtf", .rtf),
|
||||
("text/html", .html),
|
||||
("image/png", .png),
|
||||
// Original image formats pass through VERBATIM beside the PNG floor — a copied JPEG
|
||||
// never balloons into PNG, a GIF keeps its animation; the destination picks the richest
|
||||
// kind it can place.
|
||||
("image/jpeg", NSPasteboard.PasteboardType("public.jpeg")),
|
||||
("image/gif", NSPasteboard.PasteboardType("com.compuserve.gif")),
|
||||
]
|
||||
/// Pasteboard marker types that must never cross the wire (password managers mark secrets
|
||||
/// with these — see nspasteboard.org).
|
||||
@@ -205,11 +210,14 @@ public final class ClipboardSync: NSObject {
|
||||
var kinds = Self.wireToPasteboard
|
||||
.filter { types.contains($0.type) }
|
||||
.map { PunktfunkConnection.ClipKind(mime: $0.wire) }
|
||||
// Images: macOS image copies usually carry TIFF (browsers add WebP/AVIF/GIF, screenshots
|
||||
// TIFF) and only sometimes PNG — announce the portable `image/png` whenever ANY
|
||||
// convertible image type is present; `serveFetch` converts at fetch time (lazy, §3.5).
|
||||
// PNG floor: announce the portable `image/png` whenever ANY convertible image is present
|
||||
// — native PNG, TIFF/HEIC (screenshots, Preview), or a JPEG/GIF original already being
|
||||
// offered verbatim above. `readWireData` converts at fetch time (lazy, §3.5), so the
|
||||
// fallback costs nothing unless a peer actually pastes it.
|
||||
if !kinds.contains(where: { $0.mime == "image/png" }),
|
||||
types.contains(.tiff) || types.contains(NSPasteboard.PasteboardType("public.heic"))
|
||||
types.contains(.tiff)
|
||||
|| types.contains(NSPasteboard.PasteboardType("public.heic"))
|
||||
|| kinds.contains(where: { $0.mime.hasPrefix("image/") })
|
||||
{
|
||||
kinds.append(PunktfunkConnection.ClipKind(mime: "image/png"))
|
||||
}
|
||||
@@ -385,6 +393,8 @@ private final class RemoteOfferProvider: NSObject, NSPasteboardItemDataProvider
|
||||
case .rtf: return "text/rtf"
|
||||
case .html: return "text/html"
|
||||
case .png: return "image/png"
|
||||
case NSPasteboard.PasteboardType("public.jpeg"): return "image/jpeg"
|
||||
case NSPasteboard.PasteboardType("com.compuserve.gif"): return "image/gif"
|
||||
default: return nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,7 +22,7 @@ quinn = "0.11"
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "sync", "time", "macros"] }
|
||||
# CF_DIB <-> PNG conversion (winfmt) - most Windows apps paste bitmaps, not the "PNG" format.
|
||||
# Unconditional (not windows-gated) so winfmt's pure-conversion unit tests run on every host.
|
||||
image = { version = "0.25", default-features = false, features = ["png", "bmp"] }
|
||||
image = { version = "0.25", default-features = false, features = ["png", "bmp", "jpeg", "gif"] }
|
||||
|
||||
[target.'cfg(target_os = "linux")'.dependencies]
|
||||
# Mutter's direct RemoteDesktop clipboard is raw D-Bus via `ashpd::zbus` — NOT the xdg
|
||||
|
||||
@@ -235,6 +235,12 @@ pub const WIRE_HTML: &str = "text/html";
|
||||
pub const WIRE_RTF: &str = "text/rtf";
|
||||
/// Wire MIME for a PNG image.
|
||||
pub const WIRE_PNG: &str = "image/png";
|
||||
/// Wire MIME for a JPEG image — passed through VERBATIM when the source clipboard carries one
|
||||
/// (no PNG transcode: a lossy original re-encoded lossless is pure bloat). [`WIRE_PNG`] remains
|
||||
/// the universal fallback every peer must accept; JPEG/GIF are richer options beside it.
|
||||
pub const WIRE_JPEG: &str = "image/jpeg";
|
||||
/// Wire MIME for a GIF image — verbatim pass-through preserves animation end to end.
|
||||
pub const WIRE_GIF: &str = "image/gif";
|
||||
|
||||
/// Map a Wayland selection MIME to its canonical wire MIME, or `None` to drop it (internal targets
|
||||
/// like `TARGETS`/`TIMESTAMP`/`SAVE_TARGETS`, and formats we don't sync in Phase 1). Aliases
|
||||
@@ -248,6 +254,8 @@ pub fn wayland_to_wire(wl: &str) -> Option<&'static str> {
|
||||
"text/html" => Some(WIRE_HTML),
|
||||
"text/rtf" | "application/rtf" | "text/richtext" => Some(WIRE_RTF),
|
||||
"image/png" => Some(WIRE_PNG),
|
||||
"image/jpeg" => Some(WIRE_JPEG),
|
||||
"image/gif" => Some(WIRE_GIF),
|
||||
_ => match base {
|
||||
"text/plain" | "UTF8_STRING" | "STRING" | "TEXT" => Some(WIRE_TEXT),
|
||||
_ => None,
|
||||
@@ -270,6 +278,8 @@ pub fn wayland_candidates(wire: &str) -> &'static [&'static str] {
|
||||
WIRE_HTML => &["text/html"],
|
||||
WIRE_RTF => &["text/rtf", "application/rtf", "text/richtext"],
|
||||
WIRE_PNG => &["image/png"],
|
||||
WIRE_JPEG => &["image/jpeg"],
|
||||
WIRE_GIF => &["image/gif"],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
@@ -330,6 +340,8 @@ pub fn wayland_offers_for(wire_mimes: &[String]) -> Vec<String> {
|
||||
push("text/rtf");
|
||||
}
|
||||
WIRE_PNG => push("image/png"),
|
||||
WIRE_JPEG => push("image/jpeg"),
|
||||
WIRE_GIF => push("image/gif"),
|
||||
other => push(other),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,9 @@ use ::windows::Win32::UI::WindowsAndMessaging::{
|
||||
};
|
||||
|
||||
use super::winfmt;
|
||||
use super::{ClipEvent, PasteResponder, WIRE_HTML, WIRE_PNG, WIRE_RTF, WIRE_TEXT};
|
||||
use super::{
|
||||
ClipEvent, PasteResponder, WIRE_GIF, WIRE_HTML, WIRE_JPEG, WIRE_PNG, WIRE_RTF, WIRE_TEXT,
|
||||
};
|
||||
|
||||
/// Custom app message that wakes the pump to drain the [`Cmd`] channel.
|
||||
const WM_APP_CMD: u32 = WM_APP + 1;
|
||||
@@ -98,6 +100,13 @@ struct WinClip {
|
||||
fmt_html: u32,
|
||||
fmt_rtf: u32,
|
||||
fmt_png: u32,
|
||||
/// Registered `"JFIF"` — the conventional raw-JPEG clipboard format (Office/browsers).
|
||||
fmt_jfif: u32,
|
||||
/// Registered `"GIF"` — raw GIF bytes (animation preserved by verbatim pass-through).
|
||||
fmt_gif: u32,
|
||||
/// The wire MIMEs of the offer currently promised via delayed rendering — `WM_RENDERFORMAT`
|
||||
/// for the synthesized `CF_DIB` picks the richest image kind among them to fetch + convert.
|
||||
offered_wires: RefCell<Vec<String>>,
|
||||
/// Our own message window — used for the owner-check and clipboard opens.
|
||||
own_hwnd: HWND,
|
||||
}
|
||||
@@ -137,6 +146,15 @@ impl WinClip {
|
||||
if avail(self.fmt_png) || avail(CF_DIB.0 as u32) {
|
||||
out.push(WIRE_PNG.to_string());
|
||||
}
|
||||
// Original lossy/animated formats offered VERBATIM beside the PNG floor — the client
|
||||
// picks the richest kind it can place, so a copied JPEG never balloons into PNG and a
|
||||
// GIF keeps its animation.
|
||||
if avail(self.fmt_jfif) {
|
||||
out.push(WIRE_JPEG.to_string());
|
||||
}
|
||||
if avail(self.fmt_gif) {
|
||||
out.push(WIRE_GIF.to_string());
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
@@ -192,6 +210,7 @@ impl WinClip {
|
||||
}
|
||||
}
|
||||
*self.offered.borrow_mut() = fmts;
|
||||
*self.offered_wires.borrow_mut() = wire.to_vec();
|
||||
}
|
||||
|
||||
/// Drop the selection we own (empty the clipboard iff we're still its owner).
|
||||
@@ -265,8 +284,23 @@ impl WinClip {
|
||||
/// `WM_RENDERFORMAT`: a host app is pasting a format we promised. Fetch the bytes from the client
|
||||
/// (blocking this thread, bounded) and `SetClipboardData` them for the paster.
|
||||
fn on_render_format(&self, fmt: u32) {
|
||||
let Some(wire) = self.wire_for_format(fmt) else {
|
||||
return;
|
||||
// The synthesized CF_DIB promise has no wire kind of its own: fetch the richest image
|
||||
// kind the client offered (PNG first — lossless with alpha — then JPEG, then GIF's first
|
||||
// frame) and convert. Every other format maps 1:1.
|
||||
let wire: &str = if fmt == CF_DIB.0 as u32 {
|
||||
let offered = self.offered_wires.borrow();
|
||||
match [WIRE_PNG, WIRE_JPEG, WIRE_GIF]
|
||||
.into_iter()
|
||||
.find(|w| offered.iter().any(|o| o == w))
|
||||
{
|
||||
Some(w) => w,
|
||||
None => return,
|
||||
}
|
||||
} else {
|
||||
match self.wire_for_format(fmt) {
|
||||
Some(w) => w,
|
||||
None => return,
|
||||
}
|
||||
};
|
||||
let (tx, rx) = std::sync::mpsc::channel::<Vec<u8>>();
|
||||
let ev = ClipEvent::Paste {
|
||||
@@ -281,9 +315,9 @@ impl WinClip {
|
||||
Err(_) => return, // timeout / dropped → leave the format unrendered (empty paste)
|
||||
};
|
||||
let win_bytes = if fmt == CF_DIB.0 as u32 {
|
||||
// The app asked for a bitmap: the client served PNG — convert. A PNG that doesn't
|
||||
// decode leaves the format unrendered (empty paste), matching the timeout path.
|
||||
match winfmt::png_to_dib(&bytes) {
|
||||
// The app asked for a bitmap: convert whatever image kind the client served. Bytes
|
||||
// that don't decode leave the format unrendered (empty paste), like the timeout path.
|
||||
match winfmt::image_to_dib(&bytes) {
|
||||
Some(d) => d,
|
||||
None => return,
|
||||
}
|
||||
@@ -310,6 +344,8 @@ impl WinClip {
|
||||
WIRE_HTML => Some(self.fmt_html),
|
||||
WIRE_RTF => Some(self.fmt_rtf),
|
||||
WIRE_PNG => Some(self.fmt_png),
|
||||
WIRE_JPEG => Some(self.fmt_jfif),
|
||||
WIRE_GIF => Some(self.fmt_gif),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -322,8 +358,12 @@ impl WinClip {
|
||||
Some(WIRE_HTML)
|
||||
} else if fmt == self.fmt_rtf {
|
||||
Some(WIRE_RTF)
|
||||
} else if fmt == self.fmt_png || fmt == CF_DIB.0 as u32 {
|
||||
} else if fmt == self.fmt_png {
|
||||
Some(WIRE_PNG)
|
||||
} else if fmt == self.fmt_jfif {
|
||||
Some(WIRE_JPEG)
|
||||
} else if fmt == self.fmt_gif {
|
||||
Some(WIRE_GIF)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@@ -340,9 +380,12 @@ impl WinClip {
|
||||
}
|
||||
}
|
||||
// Image offers also promise CF_DIB — most pasting apps (Paint, Office, chat clients)
|
||||
// ask for the bitmap family, not the registered "PNG"; Windows synthesizes
|
||||
// CF_BITMAP/CF_DIBV5 from the promised CF_DIB. `on_render_format` converts on demand.
|
||||
if w == WIRE_PNG && !out.contains(&(CF_DIB.0 as u32)) {
|
||||
// ask for the bitmap family, not the registered image formats; Windows synthesizes
|
||||
// CF_BITMAP/CF_DIBV5 from the promised CF_DIB. `on_render_format` fetches the richest
|
||||
// offered image kind and converts on demand.
|
||||
if matches!(w.as_str(), WIRE_PNG | WIRE_JPEG | WIRE_GIF)
|
||||
&& !out.contains(&(CF_DIB.0 as u32))
|
||||
{
|
||||
out.push(CF_DIB.0 as u32);
|
||||
}
|
||||
}
|
||||
@@ -376,12 +419,18 @@ impl WindowsClipboard {
|
||||
let fmt_html = register_format(w!("HTML Format"))?;
|
||||
let fmt_rtf = register_format(w!("Rich Text Format"))?;
|
||||
let fmt_png = register_format(w!("PNG"))?;
|
||||
let fmt_jfif = register_format(w!("JFIF"))?;
|
||||
let fmt_gif = register_format(w!("GIF"))?;
|
||||
|
||||
let (ready_tx, ready_rx) = tokio::sync::oneshot::channel::<anyhow::Result<isize>>();
|
||||
let cw = Arc::clone(¤t_wire);
|
||||
let join = std::thread::Builder::new()
|
||||
.name("punktfunk-clipboard-win".into())
|
||||
.spawn(move || pump_thread(clip_tx, cmd_rx, cw, fmt_html, fmt_rtf, fmt_png, ready_tx))
|
||||
.spawn(move || {
|
||||
pump_thread(
|
||||
clip_tx, cmd_rx, cw, fmt_html, fmt_rtf, fmt_png, fmt_jfif, fmt_gif, ready_tx,
|
||||
)
|
||||
})
|
||||
.context("spawn windows clipboard thread")?;
|
||||
|
||||
let hwnd = match tokio::time::timeout(Duration::from_secs(3), ready_rx).await {
|
||||
@@ -573,6 +622,7 @@ fn create_window() -> anyhow::Result<HWND> {
|
||||
}
|
||||
|
||||
/// The message-loop thread body: build the window, wire up state, then pump until `WM_QUIT`.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn pump_thread(
|
||||
clip_tx: ClipTx,
|
||||
cmd_rx: tokio::sync::mpsc::UnboundedReceiver<Cmd>,
|
||||
@@ -580,6 +630,8 @@ fn pump_thread(
|
||||
fmt_html: u32,
|
||||
fmt_rtf: u32,
|
||||
fmt_png: u32,
|
||||
fmt_jfif: u32,
|
||||
fmt_gif: u32,
|
||||
ready_tx: tokio::sync::oneshot::Sender<anyhow::Result<isize>>,
|
||||
) {
|
||||
let hwnd = match create_window() {
|
||||
@@ -598,9 +650,12 @@ fn pump_thread(
|
||||
current_wire,
|
||||
cmd_rx: RefCell::new(cmd_rx),
|
||||
offered: RefCell::new(Vec::new()),
|
||||
offered_wires: RefCell::new(Vec::new()),
|
||||
fmt_html,
|
||||
fmt_rtf,
|
||||
fmt_png,
|
||||
fmt_jfif,
|
||||
fmt_gif,
|
||||
own_hwnd: hwnd,
|
||||
});
|
||||
let ptr = Box::into_raw(state);
|
||||
|
||||
@@ -265,7 +265,7 @@ mod tests {
|
||||
image::DynamicImage::ImageRgba8(img.clone())
|
||||
.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
|
||||
.unwrap();
|
||||
let dib = png_to_dib(&png).expect("png -> dib");
|
||||
let dib = image_to_dib(&png).expect("png -> dib");
|
||||
// BITMAPINFOHEADER sanity: 40-byte header, 3x2, 32bpp.
|
||||
assert_eq!(u32::from_le_bytes(dib[0..4].try_into().unwrap()), 40);
|
||||
assert_eq!(i32::from_le_bytes(dib[4..8].try_into().unwrap()), 3);
|
||||
@@ -290,10 +290,11 @@ mod tests {
|
||||
// the backend converts. A CF_DIB HGLOBAL is a BMP file minus its 14-byte BITMAPFILEHEADER:
|
||||
// BITMAPINFOHEADER (or V4/V5) + optional palette/masks + pixel rows.
|
||||
|
||||
/// PNG wire bytes → `CF_DIB` HGLOBAL bytes (BITMAPINFOHEADER, 32bpp BGRA, BI_RGB, bottom-up).
|
||||
/// `None` when the PNG doesn't decode — the caller leaves the format unrendered (empty paste).
|
||||
pub fn png_to_dib(png: &[u8]) -> Option<Vec<u8>> {
|
||||
let img = image::load_from_memory_with_format(png, image::ImageFormat::Png).ok()?;
|
||||
/// Image wire bytes (PNG / JPEG / GIF — any format the `image` crate sniffs) → `CF_DIB` HGLOBAL
|
||||
/// bytes (BITMAPINFOHEADER, 32bpp BGRA, BI_RGB, bottom-up). GIFs contribute their first frame.
|
||||
/// `None` when the bytes don't decode — the caller leaves the format unrendered (empty paste).
|
||||
pub fn image_to_dib(bytes: &[u8]) -> Option<Vec<u8>> {
|
||||
let img = image::load_from_memory(bytes).ok()?;
|
||||
let rgba = img.to_rgba8();
|
||||
let (w, h) = (rgba.width() as usize, rgba.height() as usize);
|
||||
if w == 0 || h == 0 || w > 32767 || h > 32767 {
|
||||
|
||||
Reference in New Issue
Block a user