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
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:
@@ -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]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user