fix(pf-clipboard): winfmt layout + wayland map test for the JPEG/GIF pass-through
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 58s
apple / swift (push) Successful in 1m23s
decky / build-publish (push) Successful in 22s
docker / build-push (--build-arg FEDORA_VERSION=44, ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm) (push) Successful in 11s
docker / build-push (., web/Dockerfile, punktfunk-web) (push) Successful in 10s
docker / build-push (ci, ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 10s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 11s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 15s
ci / bench (push) Successful in 6m25s
arch / build-publish (push) Successful in 11m18s
android / android (push) Successful in 13m57s
ci / rust (push) Has been cancelled
deb / build-publish (push) Has been cancelled
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Has been cancelled
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Has been cancelled
apple / screenshots (push) Has been cancelled
docker / deploy-docs (push) Has been cancelled

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-17 19:33:51 +02:00
parent f4b52d0bf5
commit 055b95a181
2 changed files with 87 additions and 84 deletions
+4 -1
View File
@@ -367,10 +367,13 @@ mod tests {
assert_eq!(wayland_to_wire("text/html"), Some(WIRE_HTML));
assert_eq!(wayland_to_wire("application/rtf"), Some(WIRE_RTF));
assert_eq!(wayland_to_wire("image/png"), Some(WIRE_PNG));
// Original image formats now map to their own wire kinds (verbatim pass-through).
assert_eq!(wayland_to_wire("image/jpeg"), Some(WIRE_JPEG));
assert_eq!(wayland_to_wire("image/gif"), Some(WIRE_GIF));
// Internal targets and unsupported formats are dropped.
assert_eq!(wayland_to_wire("TARGETS"), None);
assert_eq!(wayland_to_wire("TIMESTAMP"), None);
assert_eq!(wayland_to_wire("image/jpeg"), None);
assert_eq!(wayland_to_wire("image/webp"), None);
}
#[test]
+83 -83
View File
@@ -158,6 +158,89 @@ fn strip_trailing_nul(b: &[u8]) -> &[u8] {
}
}
// ---- CF_DIB ↔ image/png ------------------------------------------------------------------------
//
// Most Windows apps speak the bitmap clipboard family (`CF_DIB`, from which Windows synthesizes
// `CF_BITMAP`/`CF_DIBV5`), not the registered `"PNG"` format — so images only interoperate when
// 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.
/// 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 {
return None;
}
let mut out = Vec::with_capacity(40 + w * h * 4);
// BITMAPINFOHEADER: 32bpp BI_RGB needs no masks/palette; positive height = bottom-up rows.
out.extend_from_slice(&40u32.to_le_bytes()); // biSize
out.extend_from_slice(&(w as i32).to_le_bytes()); // biWidth
out.extend_from_slice(&(h as i32).to_le_bytes()); // biHeight (bottom-up)
out.extend_from_slice(&1u16.to_le_bytes()); // biPlanes
out.extend_from_slice(&32u16.to_le_bytes()); // biBitCount
out.extend_from_slice(&0u32.to_le_bytes()); // biCompression = BI_RGB
out.extend_from_slice(&((w * h * 4) as u32).to_le_bytes()); // biSizeImage
out.extend_from_slice(&[0u8; 16]); // XPels/YPels/ClrUsed/ClrImportant
// Rows bottom-up, pixels BGRA (32bpp rows are already 4-byte aligned).
for row in rgba.rows().rev() {
for px in row {
let [r, g, b, a] = px.0;
out.extend_from_slice(&[b, g, r, a]);
}
}
Some(out)
}
/// `CF_DIB` HGLOBAL bytes → PNG wire bytes: prepend the BITMAPFILEHEADER a BMP decoder expects
/// (computing the pixel offset from the header + palette/mask layout) and re-encode. `None` on
/// any malformed input — the caller declines the fetch.
pub fn dib_to_png(dib: &[u8]) -> Option<Vec<u8>> {
if dib.len() < 40 {
return None;
}
let hdr_size = u32::from_le_bytes(dib[0..4].try_into().ok()?) as usize;
if hdr_size < 40 || hdr_size > dib.len() {
return None;
}
let bit_count = u16::from_le_bytes(dib[14..16].try_into().ok()?) as usize;
let compression = u32::from_le_bytes(dib[16..20].try_into().ok()?);
let clr_used = u32::from_le_bytes(dib[32..36].try_into().ok()?) as usize;
// Palette entries (≤8bpp) or BI_BITFIELDS masks follow the header before the pixels.
let palette = if bit_count <= 8 {
(if clr_used != 0 {
clr_used
} else {
1 << bit_count
}) * 4
} else if compression == 3 {
// BI_BITFIELDS: 3 DWORD masks (only when the header is a plain BITMAPINFOHEADER —
// V4/V5 headers already contain the masks within hdr_size).
if hdr_size == 40 {
12
} else {
0
}
} else {
0
};
let pixel_offset = 14 + hdr_size + palette;
let mut bmp = Vec::with_capacity(14 + dib.len());
bmp.extend_from_slice(b"BM");
bmp.extend_from_slice(&((14 + dib.len()) as u32).to_le_bytes());
bmp.extend_from_slice(&0u32.to_le_bytes()); // reserved
bmp.extend_from_slice(&(pixel_offset as u32).to_le_bytes());
bmp.extend_from_slice(dib);
let img = image::load_from_memory_with_format(&bmp, image::ImageFormat::Bmp).ok()?;
let mut png = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
.ok()?;
Some(png)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -282,86 +365,3 @@ mod tests {
assert!(dib_to_png(&[0xFFu8; 200]).is_none());
}
}
// ---- CF_DIB ↔ image/png ------------------------------------------------------------------------
//
// Most Windows apps speak the bitmap clipboard family (`CF_DIB`, from which Windows synthesizes
// `CF_BITMAP`/`CF_DIBV5`), not the registered `"PNG"` format — so images only interoperate when
// 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.
/// 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 {
return None;
}
let mut out = Vec::with_capacity(40 + w * h * 4);
// BITMAPINFOHEADER: 32bpp BI_RGB needs no masks/palette; positive height = bottom-up rows.
out.extend_from_slice(&40u32.to_le_bytes()); // biSize
out.extend_from_slice(&(w as i32).to_le_bytes()); // biWidth
out.extend_from_slice(&(h as i32).to_le_bytes()); // biHeight (bottom-up)
out.extend_from_slice(&1u16.to_le_bytes()); // biPlanes
out.extend_from_slice(&32u16.to_le_bytes()); // biBitCount
out.extend_from_slice(&0u32.to_le_bytes()); // biCompression = BI_RGB
out.extend_from_slice(&((w * h * 4) as u32).to_le_bytes()); // biSizeImage
out.extend_from_slice(&[0u8; 16]); // XPels/YPels/ClrUsed/ClrImportant
// Rows bottom-up, pixels BGRA (32bpp rows are already 4-byte aligned).
for row in rgba.rows().rev() {
for px in row {
let [r, g, b, a] = px.0;
out.extend_from_slice(&[b, g, r, a]);
}
}
Some(out)
}
/// `CF_DIB` HGLOBAL bytes → PNG wire bytes: prepend the BITMAPFILEHEADER a BMP decoder expects
/// (computing the pixel offset from the header + palette/mask layout) and re-encode. `None` on
/// any malformed input — the caller declines the fetch.
pub fn dib_to_png(dib: &[u8]) -> Option<Vec<u8>> {
if dib.len() < 40 {
return None;
}
let hdr_size = u32::from_le_bytes(dib[0..4].try_into().ok()?) as usize;
if hdr_size < 40 || hdr_size > dib.len() {
return None;
}
let bit_count = u16::from_le_bytes(dib[14..16].try_into().ok()?) as usize;
let compression = u32::from_le_bytes(dib[16..20].try_into().ok()?);
let clr_used = u32::from_le_bytes(dib[32..36].try_into().ok()?) as usize;
// Palette entries (≤8bpp) or BI_BITFIELDS masks follow the header before the pixels.
let palette = if bit_count <= 8 {
(if clr_used != 0 {
clr_used
} else {
1 << bit_count
}) * 4
} else if compression == 3 {
// BI_BITFIELDS: 3 DWORD masks (only when the header is a plain BITMAPINFOHEADER —
// V4/V5 headers already contain the masks within hdr_size).
if hdr_size == 40 {
12
} else {
0
}
} else {
0
};
let pixel_offset = 14 + hdr_size + palette;
let mut bmp = Vec::with_capacity(14 + dib.len());
bmp.extend_from_slice(b"BM");
bmp.extend_from_slice(&((14 + dib.len()) as u32).to_le_bytes());
bmp.extend_from_slice(&0u32.to_le_bytes()); // reserved
bmp.extend_from_slice(&(pixel_offset as u32).to_le_bytes());
bmp.extend_from_slice(dib);
let img = image::load_from_memory_with_format(&bmp, image::ImageFormat::Bmp).ok()?;
let mut png = Vec::new();
img.write_to(&mut std::io::Cursor::new(&mut png), image::ImageFormat::Png)
.ok()?;
Some(png)
}