Fix sticker capture and rendering
This commit is contained in:
+166
-23
@@ -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.`);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user