Fix sticker capture and rendering

This commit is contained in:
sevenhill
2026-07-06 13:37:07 +03:00
parent 30361370da
commit e50b99bcd2
3 changed files with 223 additions and 24 deletions
+166 -23
View File
@@ -1190,9 +1190,135 @@ async function downloadDomAudioThroughPlay(p, request) {
async function downloadDomStickerThroughScreenshot(p, request) {
const testId = String(request?.testId || "").trim();
const timeText = String(request?.timeText || "").trim();
const fallbackSelector = String(request?.selector || "[data-testid^='sticker-'], button[class*='sticker' i], [class*='sticker' i] canvas, canvas");
const fallbackSelector = String(request?.selector || "[data-testid^='sticker-'], button[class*='sticker' i], [class*='sticker' i]");
const kindLabel = String(request?.kindLabel || "MAX sticker canvas");
const selectorForTestId = (value) => `[data-testid="${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"]`;
const visualHandleFor = async (handle) => {
const visual = await handle.evaluateHandle((node) => {
const element = node instanceof Element ? node : node?.parentElement;
if (!element) return null;
const candidates = [
...(element.matches("canvas,img,image,svg") ? [element] : []),
...Array.from(element.querySelectorAll("canvas,img,image,svg,[style*='background' i]"))
];
const scored = candidates
.map((candidate) => {
const rect = candidate.getBoundingClientRect();
const style = window.getComputedStyle(candidate);
if (style.visibility === "hidden" ||
style.display === "none" ||
rect.width <= 0 ||
rect.height <= 0) {
return null;
}
const semantic = [
candidate.getAttribute?.("data-testid"),
candidate.getAttribute?.("aria-label"),
candidate.getAttribute?.("title"),
candidate.className?.baseVal || candidate.className,
candidate.tagName
].filter(Boolean).join(" ").toLowerCase();
let score = rect.width * rect.height;
if (candidate.matches("canvas,img,image,svg")) score += 100000;
if (/sticker|emoji|lottie|emoticon|\u0441\u0442\u0438\u043a\u0435\u0440|\u044d\u043c\u043e\u0434\u0437\u0438/.test(semantic)) score += 50000;
return { candidate, score };
})
.filter(Boolean)
.sort((a, b) => b.score - a.score);
return scored[0]?.candidate || element;
}).catch(() => null);
const element = visual?.asElement?.();
if (element) {
return element;
}
await visual?.dispose?.().catch(() => {});
return handle;
};
const resolveVisualHandles = async (handles) => {
const resolved = [];
for (const handle of handles) {
const visual = await visualHandleFor(handle);
if (visual !== handle) {
await handle.dispose().catch(() => {});
}
resolved.push(visual);
}
return resolved;
};
const captureCanvasPng = async (handle) => {
const result = await handle.evaluate(async (node) => {
const element = node instanceof Element ? node : node?.parentElement;
const canvas = element?.matches?.("canvas")
? element
: element?.querySelector?.("canvas");
if (!(canvas instanceof HTMLCanvasElement)) {
return { hasCanvas: false, blocked: false, dataUrl: null };
}
const sleep = (ms) => new Promise((resolve) => window.setTimeout(resolve, ms));
const hasPaintedPixels = () => {
const width = canvas.width;
const height = canvas.height;
if (width <= 0 || height <= 0) return false;
let data;
try {
const context = canvas.getContext("2d", { willReadFrequently: true });
if (!context) return true;
data = context.getImageData(0, 0, width, height).data;
} catch (_error) {
return null;
}
const totalPixels = width * height;
const step = Math.max(1, Math.floor(totalPixels / 12000));
let painted = 0;
for (let pixel = 0; pixel < totalPixels; pixel += step) {
if (data[pixel * 4 + 3] > 8) {
painted++;
}
}
return painted >= Math.max(6, Math.ceil(totalPixels / step * 0.01));
};
for (let attempt = 0; attempt < 10; attempt++) {
const painted = hasPaintedPixels();
if (painted === true) {
try {
return { hasCanvas: true, blocked: false, dataUrl: canvas.toDataURL("image/png") };
} catch (_error) {
return { hasCanvas: true, blocked: true, dataUrl: null };
}
}
if (painted === null) {
try {
return { hasCanvas: true, blocked: true, dataUrl: canvas.toDataURL("image/png") };
} catch (_error) {
return { hasCanvas: true, blocked: true, dataUrl: null };
}
}
await sleep(200);
}
return { hasCanvas: true, blocked: false, dataUrl: null };
}).catch(() => ({ hasCanvas: false, blocked: false, dataUrl: null }));
const dataUrl = String(result?.dataUrl || "");
const prefix = "data:image/png;base64,";
return {
hasCanvas: Boolean(result?.hasCanvas),
blocked: Boolean(result?.blocked),
buffer: dataUrl.startsWith(prefix) ? Buffer.from(dataUrl.slice(prefix.length), "base64") : null
};
};
const screenshotCandidate = async (handles, requestedIndex) => {
const candidates = [];
for (const handle of handles) {
@@ -1234,11 +1360,16 @@ async function downloadDomStickerThroughScreenshot(p, request) {
.join(" ");
const timeMatches = requestedTimeText &&
(wrapperText.includes(requestedTimeText) || metaText.includes(requestedTimeText));
const semanticRoot = element.closest("[data-testid^='sticker-'], [data-testid*='emoji' i], [class*='sticker' i], [class*='emoji' i], [class*='lottie' i]") || element;
const semantic = [
element.getAttribute("data-testid"),
element.getAttribute("aria-label"),
element.getAttribute("title"),
element.className,
semanticRoot.getAttribute?.("data-testid"),
semanticRoot.getAttribute?.("aria-label"),
semanticRoot.getAttribute?.("title"),
semanticRoot.className,
wrapper.className,
wrapperText
].filter(Boolean).join(" ").toLowerCase();
@@ -1328,47 +1459,59 @@ async function downloadDomStickerThroughScreenshot(p, request) {
const box = await target.handle.boundingBox().catch(() => null);
return box && box.width > 0 && box.height > 0 ? box : metaBox;
};
await target.handle.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
let buffer;
if (request?.acceptSmall) {
buffer = await screenshotClip(await currentBoxOrMeta());
if (!buffer) {
throw new Error(`${kindLabel} clip was not visible.`);
try {
await target.handle.scrollIntoViewIfNeeded({ timeout: 3000 }).catch(() => {});
const canvasCapture = await captureCanvasPng(target.handle);
if (canvasCapture.buffer) {
return { contentType: "image/png", buffer: canvasCapture.buffer };
}
} else {
try {
buffer = await target.handle.screenshot({ type: "png", timeout: 5000 });
} catch (error) {
if (canvasCapture.hasCanvas && !canvasCapture.blocked) {
throw new Error(`${kindLabel} canvas is blank.`);
}
let buffer;
if (request?.acceptSmall) {
buffer = await screenshotClip(await currentBoxOrMeta());
if (!buffer) {
throw new Error(`${kindLabel} clip was not visible.`);
}
} else {
try {
buffer = await target.handle.screenshot({ type: "png", timeout: 5000 });
} catch (error) {
buffer = await screenshotClip(await currentBoxOrMeta());
if (!buffer) {
throw error;
}
}
}
if (!buffer) {
try {
buffer = await target.handle.screenshot({ type: "png", timeout: 5000 });
} catch (error) {
throw error;
}
}
}
if (!buffer) {
try {
buffer = await target.handle.screenshot({ type: "png", timeout: 5000 });
} catch (error) {
throw error;
return { contentType: "image/png", buffer };
} finally {
for (const item of candidates) {
await item.handle.dispose().catch(() => {});
}
}
for (const item of candidates) {
await item.handle.dispose().catch(() => {});
}
return { contentType: "image/png", buffer };
};
const index = Number.isFinite(Number(request?.index)) ? Number(request.index) : 0;
if (testId) {
const handles = await p.$$(selectorForTestId(testId));
const result = await screenshotCandidate(handles, 0);
const result = await screenshotCandidate(await resolveVisualHandles(handles), 0);
if (result) {
return result;
}
}
const handles = await p.$$(fallbackSelector);
const result = await screenshotCandidate(handles, index);
const result = await screenshotCandidate(await resolveVisualHandles(handles), index);
if (!result) {
throw new Error(`${kindLabel} was not found.`);
}