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("text/html"), Some(WIRE_HTML));
|
||||||
assert_eq!(wayland_to_wire("application/rtf"), Some(WIRE_RTF));
|
assert_eq!(wayland_to_wire("application/rtf"), Some(WIRE_RTF));
|
||||||
assert_eq!(wayland_to_wire("image/png"), Some(WIRE_PNG));
|
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.
|
// Internal targets and unsupported formats are dropped.
|
||||||
assert_eq!(wayland_to_wire("TARGETS"), None);
|
assert_eq!(wayland_to_wire("TARGETS"), None);
|
||||||
assert_eq!(wayland_to_wire("TIMESTAMP"), 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]
|
#[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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
@@ -282,86 +365,3 @@ mod tests {
|
|||||||
assert!(dib_to_png(&[0xFFu8; 200]).is_none());
|
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