fix(apple/clipboard): announce + serve image/png from ANY pasteboard image type
apple / swift (push) Successful in 1m20s
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 1m4s
decky / build-publish (push) Successful in 19s
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 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m43s
release / apple (push) Successful in 5m48s
docker / deploy-docs (push) Successful in 26s
apple / screenshots (push) Successful in 6m26s
android / android (push) Successful in 13m58s
arch / build-publish (push) Successful in 14m37s
ci / rust (push) Failing after 15m36s
deb / build-publish (push) Successful in 14m40s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m24s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m45s
apple / swift (push) Successful in 1m20s
ci / web (push) Successful in 49s
ci / docs-site (push) Successful in 1m4s
decky / build-publish (push) Successful in 19s
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 9s
docker / build-push (ci, ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 10s
docker / build-push (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
ci / bench (push) Successful in 5m43s
release / apple (push) Successful in 5m48s
docker / deploy-docs (push) Successful in 26s
apple / screenshots (push) Successful in 6m26s
android / android (push) Successful in 13m58s
arch / build-publish (push) Successful in 14m37s
ci / rust (push) Failing after 15m36s
deb / build-publish (push) Successful in 14m40s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 14m24s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 14m45s
macOS image copies rarely carry public.png — screenshots/Preview put TIFF on the pasteboard, browsers add WebP/AVIF/GIF (observed live: TIFF+RTFD+WebP+AVIF+ 8BPS+GIF, no PNG) — so the literal .png announce never fired and images silently didn't sync. Announce image/png whenever a convertible image is present (TIFF/HEIC alongside the native PNG check) and convert at serve time via NSImage -> NSBitmapImageRep PNG (lazy, per design §3.5 — bytes still cross only on a host paste). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -202,9 +202,17 @@ public final class ClipboardSync: NSObject {
|
|||||||
let types = pb.types ?? []
|
let types = pb.types ?? []
|
||||||
if types.contains(Self.concealed) || types.contains(Self.transient) { return }
|
if types.contains(Self.concealed) || types.contains(Self.transient) { return }
|
||||||
offerSeq &+= 1
|
offerSeq &+= 1
|
||||||
let kinds = Self.wireToPasteboard
|
var kinds = Self.wireToPasteboard
|
||||||
.filter { types.contains($0.type) }
|
.filter { types.contains($0.type) }
|
||||||
.map { PunktfunkConnection.ClipKind(mime: $0.wire) }
|
.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).
|
||||||
|
if !kinds.contains(where: { $0.mime == "image/png" }),
|
||||||
|
types.contains(.tiff) || types.contains(NSPasteboard.PasteboardType("public.heic"))
|
||||||
|
{
|
||||||
|
kinds.append(PunktfunkConnection.ClipKind(mime: "image/png"))
|
||||||
|
}
|
||||||
// Empty = the pasteboard holds nothing we sync (or was cleared) — clears the host side.
|
// Empty = the pasteboard holds nothing we sync (or was cleared) — clears the host side.
|
||||||
connection.clipOffer(seq: offerSeq, kinds: kinds)
|
connection.clipOffer(seq: offerSeq, kinds: kinds)
|
||||||
}
|
}
|
||||||
@@ -305,8 +313,7 @@ public final class ClipboardSync: NSObject {
|
|||||||
private func serveFetch(reqId: UInt32, seq: UInt32, mime: String) {
|
private func serveFetch(reqId: UInt32, seq: UInt32, mime: String) {
|
||||||
let pb = NSPasteboard.general
|
let pb = NSPasteboard.general
|
||||||
guard seq == offerSeq, pb.changeCount == lastSeenChangeCount,
|
guard seq == offerSeq, pb.changeCount == lastSeenChangeCount,
|
||||||
let type = Self.wireToPasteboard.first(where: { $0.wire == mime })?.type,
|
let data = Self.readWireData(pb, mime)
|
||||||
let data = pb.data(forType: type)
|
|
||||||
else {
|
else {
|
||||||
connection.clipCancel(id: reqId)
|
connection.clipCancel(id: reqId)
|
||||||
return
|
return
|
||||||
@@ -322,6 +329,30 @@ public final class ClipboardSync: NSObject {
|
|||||||
connection.clipServe(reqId: reqId, data: Data(), last: true)
|
connection.clipServe(reqId: reqId, data: Data(), last: true)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Read one wire format from the pasteboard, converting where macOS stores a different
|
||||||
|
/// native type: `image/png` is served from a real `.png` entry when present, else converted
|
||||||
|
/// from whatever image representation the pasteboard holds (TIFF from screenshots/Preview,
|
||||||
|
/// WebP/AVIF/GIF from browsers — `NSImage` decodes them all) into PNG at fetch time.
|
||||||
|
private static func readWireData(_ pb: NSPasteboard, _ mime: String) -> Data? {
|
||||||
|
guard mime == "image/png" else {
|
||||||
|
guard let type = wireToPasteboard.first(where: { $0.wire == mime })?.type else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return pb.data(forType: type)
|
||||||
|
}
|
||||||
|
if let png = pb.data(forType: .png) {
|
||||||
|
return png
|
||||||
|
}
|
||||||
|
// No native PNG: decode whatever image the pasteboard carries and re-encode.
|
||||||
|
guard let img = NSImage(pasteboard: pb),
|
||||||
|
let tiff = img.tiffRepresentation,
|
||||||
|
let rep = NSBitmapImageRep(data: tiff)
|
||||||
|
else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return rep.representation(using: .png, properties: [:])
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The lazy paste hook: AppKit calls `provideDataForType` only when a Mac app actually pastes;
|
/// The lazy paste hook: AppKit calls `provideDataForType` only when a Mac app actually pastes;
|
||||||
|
|||||||
Reference in New Issue
Block a user