(function () { "use strict"; const SKIP_TAGS = new Set(["SCRIPT", "STYLE", "NOSCRIPT", "META", "LINK", "TEMPLATE"]); const STYLE_PROPS = [ ["display", "display"], ["visibility", "visibility"], ["position", "position"], ["zIndex", "z-index"], ["opacity", "opacity"], ["overflow", "overflow"], ["overflowX", "overflow-x"], ["overflowY", "overflow-y"], ["backgroundColor", "background-color"], ["backgroundImage", "background-image"], ["backgroundPosition", "background-position"], ["backgroundSize", "background-size"], ["backgroundRepeat", "background-repeat"], ["backgroundClip", "background-clip"], ["backgroundOrigin", "background-origin"], ["objectFit", "object-fit"], ["objectPosition", "object-position"], ["maskImage", "mask-image"], ["webkitMaskImage", "-webkit-mask-image"], ["maskPosition", "mask-position"], ["webkitMaskPosition", "-webkit-mask-position"], ["maskSize", "mask-size"], ["webkitMaskSize", "-webkit-mask-size"], ["borderTopWidth", "border-top-width"], ["borderRightWidth", "border-right-width"], ["borderBottomWidth", "border-bottom-width"], ["borderLeftWidth", "border-left-width"], ["borderTopColor", "border-top-color"], ["borderRightColor", "border-right-color"], ["borderBottomColor", "border-bottom-color"], ["borderLeftColor", "border-left-color"], ["borderTopStyle", "border-top-style"], ["borderRightStyle", "border-right-style"], ["borderBottomStyle", "border-bottom-style"], ["borderLeftStyle", "border-left-style"], ["borderTopLeftRadius", "border-top-left-radius"], ["borderTopRightRadius", "border-top-right-radius"], ["borderBottomRightRadius", "border-bottom-right-radius"], ["borderBottomLeftRadius", "border-bottom-left-radius"], ["boxShadow", "box-shadow"], ["filter", "filter"], ["backdropFilter", "backdrop-filter"], ["color", "color"], ["fontFamily", "font-family"], ["fontSize", "font-size"], ["fontWeight", "font-weight"], ["fontStyle", "font-style"], ["lineHeight", "line-height"], ["letterSpacing", "letter-spacing"], ["textAlign", "text-align"], ["textTransform", "text-transform"], ["textDecorationLine", "text-decoration-line"], ["whiteSpace", "white-space"], ["wordBreak", "word-break"], ["transform", "transform"], ["transformOrigin", "transform-origin"], ["clipPath", "clip-path"] ]; const LAYOUT_PROPS = [ ["flexDirection", "flex-direction"], ["flexWrap", "flex-wrap"], ["justifyContent", "justify-content"], ["alignItems", "align-items"], ["alignContent", "align-content"], ["gap", "gap"], ["rowGap", "row-gap"], ["columnGap", "column-gap"], ["gridTemplateColumns", "grid-template-columns"], ["gridTemplateRows", "grid-template-rows"], ["gridAutoFlow", "grid-auto-flow"], ["paddingTop", "padding-top"], ["paddingRight", "padding-right"], ["paddingBottom", "padding-bottom"], ["paddingLeft", "padding-left"], ["marginTop", "margin-top"], ["marginRight", "margin-right"], ["marginBottom", "margin-bottom"], ["marginLeft", "margin-left"], ["width", "width"], ["height", "height"], ["minWidth", "min-width"], ["maxWidth", "max-width"], ["minHeight", "min-height"], ["maxHeight", "max-height"] ]; let nodeCounter = 0; let rasterCounter = 0; const CAPTURE_SCHEMA = "web-to-ppt-capture"; const CAPTURE_SCHEMA_VERSION = "1.1.1"; function isWebToPPTElement(element) { if (!element?.closest) return false; return Boolean(element.closest([ "#__web_to_ppt_panel_root__", "#__web_to_ppt_picker_root__", "#__web_to_ppt_picker_box__", "#__web_to_ppt_picker_label__" ].join(","))); } function absoluteRect(rect, style) { const isViewportAnchored = style?.position === "fixed"; return { x: round(rect.left + (isViewportAnchored ? 0 : window.scrollX)), y: round(rect.top + (isViewportAnchored ? 0 : window.scrollY)), width: round(rect.width), height: round(rect.height) }; } function round(value) { return Math.round((Number(value) || 0) * 100) / 100; } function isVisible(element, rect, style) { if (!rect || rect.width <= 0 || rect.height <= 0) return false; if (style.display === "none" || style.visibility === "hidden") return false; if (Number(style.opacity) === 0) return false; return true; } function intersectsDocument(rect) { const documentRect = getDocumentRect(); return intersectRects(rect, documentRect); } function intersectWithClip(rect, clipRect) { return clipRect ? intersectRects(rect, clipRect) : intersectsDocument(rect); } function intersectRects(a, b) { if (!a) return b || null; if (!b) return a || null; const x1 = Math.max(a.x, b.x); const y1 = Math.max(a.y, b.y); const x2 = Math.min(a.x + a.width, b.x + b.width); const y2 = Math.min(a.y + a.height, b.y + b.height); if (x2 <= x1 || y2 <= y1) return null; return { x: round(x1), y: round(y1), width: round(x2 - x1), height: round(y2 - y1) }; } function rectsDiffer(a, b, tolerance = 0.5) { if (!a || !b) return false; return Math.abs((a.x || 0) - (b.x || 0)) > tolerance || Math.abs((a.y || 0) - (b.y || 0)) > tolerance || Math.abs((a.width || 0) - (b.width || 0)) > tolerance || Math.abs((a.height || 0) - (b.height || 0)) > tolerance; } function clipsChildren(styles) { const overflow = `${styles.overflow || ""} ${styles.overflowX || ""} ${styles.overflowY || ""}`; return /(hidden|clip|scroll|auto)/i.test(overflow); } function isCarouselClone(element) { const className = String(element.className || ""); if (/(^|\s)(slick-cloned|swiper-slide-duplicate|swiper-slide-duplicate-active|swiper-slide-duplicate-next|swiper-slide-duplicate-prev)(\s|$)/i.test(className)) { return true; } if (element.getAttribute("data-swiper-slide-index") && /swiper-slide-duplicate/i.test(className)) { return true; } if (element.getAttribute("aria-hidden") === "true" && /(^|\s)(slick-slide|swiper-slide)(\s|$)/i.test(className)) { return true; } return false; } function shouldClampToClip(element, rect, clipRect) { if (!clipRect || !rect) return false; const className = String(element.className || ""); const isTrack = /(slick-track|swiper-wrapper|carousel|marquee|scroll|slider|slide-track)/i.test(className); const isMedia = /^(IMG|PICTURE|VIDEO|CANVAS)$/i.test(element.tagName); const isClippedMedia = isMedia && rectsDiffer(rect, clipRect); const isVeryWide = rect.width > Math.max(window.innerWidth * 1.5, clipRect.width * 1.5); const isVeryTall = rect.height > Math.max(window.innerHeight * 2, clipRect.height * 2); return isTrack || isClippedMedia || isVeryWide || isVeryTall; } function getChildNodes(element) { const nodes = Array.from(element.childNodes || []); if (element.shadowRoot) { nodes.push(...Array.from(element.shadowRoot.childNodes || [])); } return nodes; } function isHeaderLikeElement(element) { if (!element) return false; const name = `${element.tagName || ""} ${element.id || ""} ${element.className || ""} ${element.getAttribute?.("role") || ""}`; return /^(HEADER|NAV)$/i.test(element.tagName || "") || /header|nav|navigation|topbar|navbar|menu/i.test(String(name)); } function isTopHeaderElement(element, rect, styles) { if (!isHeaderLikeElement(element)) return false; const tag = element?.tagName || ""; const name = `${tag} ${element?.id || ""} ${element?.className || ""} ${element?.getAttribute?.("role") || ""}`; const fixedOrSticky = styles?.position === "fixed" || styles?.position === "sticky"; const nearTop = (rect?.y || 0) <= 140 || fixedOrSticky; const headerSized = (rect?.height || 0) >= 20 && (rect?.height || 0) <= Math.max(180, window.innerHeight * 0.18) && (rect?.width || 0) >= window.innerWidth * 0.35; const strongSemantic = /^(HEADER)$/i.test(tag) || /cloud-header|site-header|page-header|topbar|navbar|navigation/i.test(String(name)); const navSemantic = /^(NAV)$/i.test(tag) || /nav|menu/i.test(String(name)); return nearTop && headerSized && (strongSemantic || navSemantic); } function inferSection(element, rect, styles, inheritedSection) { if (inheritedSection && inheritedSection !== "document") return inheritedSection; const tag = element?.tagName || ""; const name = `${tag} ${element?.id || ""} ${element?.className || ""} ${element?.getAttribute?.("role") || ""}`; if (isTopHeaderElement(element, rect, styles)) return "header"; if (/^(FOOTER)$/i.test(tag) || /footer/i.test(String(name))) return "footer"; if (rect?.y < Math.max(window.innerHeight * 1.4, 900) && /hero|banner|kv|carousel|swiper|slick|slider|slide|campaign|masthead/i.test(String(name))) { return "hero"; } if (styles && isLikelyHeroOrCarousel(element, rect, styles)) return "hero"; return inheritedSection || "content"; } function layerMetaFor(type, options = {}) { if (options.rasterMode === "comparison") { return { layerGroup: "comparison", layerPriority: 0, rasterRole: "comparison" }; } if (options.rasterMode) { return { layerGroup: "comparison", layerPriority: 0, rasterRole: options.rasterMode }; } if (type === "TEXT") { return { layerGroup: "text", layerPriority: 200 }; } return { layerGroup: "editable", layerPriority: 100 }; } function inferRasterSection(target = {}) { const name = String(target.name || ""); const mode = String(target.mode || ""); if (/header/i.test(name)) return "header"; if (/footer/i.test(name)) return "footer"; if (/hero|carousel/i.test(name) || mode === "hero-background") return "hero"; if (mode === "comparison") return "document"; return "content"; } function getDocumentRect() { const doc = document.documentElement; const body = document.body; return { x: 0, y: 0, width: Math.max(window.innerWidth, doc.clientWidth), height: Math.max(doc.scrollHeight, body.scrollHeight, doc.clientHeight) }; } function getViewportRect() { return { x: window.scrollX, y: window.scrollY, width: window.innerWidth, height: window.innerHeight }; } function extractStyles(computed) { const styles = {}; for (const [key, cssProp] of STYLE_PROPS) { styles[key] = computed.getPropertyValue(cssProp); } return styles; } function parsePixels(value, fallback = 0) { const parsed = parseFloat(value); return Number.isFinite(parsed) ? parsed : fallback; } function countDistinctPositions(rects, key, tolerance = 6) { const positions = []; for (const rect of rects || []) { const value = rect?.[key]; if (!Number.isFinite(value)) continue; if (!positions.some(position => Math.abs(position - value) <= tolerance)) { positions.push(value); } } return positions.length; } function inferGridMetrics(childRects, gap = 0) { const tolerance = Math.max(6, Math.min(18, Number(gap || 0) * 0.5)); const columns = countDistinctPositions(childRects, "x", tolerance); const rows = countDistinctPositions(childRects, "y", tolerance); return { columnCount: Math.max(1, columns || 1), rowCount: Math.max(1, rows || 1) }; } function isGridLikeLayout({ isGrid, isFlex, isTable, semanticLayout, styles, childRects, inferredGrid }) { if (isGrid) return true; if (!childRects?.length || childRects.length < 4) return false; const hasRowsAndColumns = inferredGrid.columnCount >= 2 && inferredGrid.rowCount >= 2; if (!hasRowsAndColumns) return false; const wraps = isFlex && /wrap/i.test(styles.flexWrap || ""); const semanticGrid = semanticLayout && /grid|cards|card|product|goods|coupon|list|items|row|cols|columns|footer/i.test(String(styles.className || "")); const visuallyGrid = inferredGrid.columnCount * inferredGrid.rowCount >= Math.max(4, childRects.length * 0.7); return !isTable && visuallyGrid && (wraps || semanticLayout || semanticGrid); } function extractLayout(computed, element, rect) { const display = computed.getPropertyValue("display"); const isFlex = /\b(inline-)?flex\b/i.test(display); const isGrid = /\b(inline-)?grid\b/i.test(display); const isTable = /\btable\b/i.test(display); const children = Array.from(element.children || []).filter(child => { const childRect = child.getBoundingClientRect(); const childStyle = window.getComputedStyle(child); return isVisible(child, childRect, childStyle); }); const hasLayoutChildren = children.length >= 2; const className = String(element.className || ""); const semanticLayout = /nav|menu|list|grid|row|column|cols|cards|items|tabs|footer|header|toolbar|form|actions|buttons/i.test(className); if (!isFlex && !isGrid && !isTable && !semanticLayout) return null; if (!hasLayoutChildren && !isFlex && !isGrid) return null; const styles = {}; for (const [key, cssProp] of LAYOUT_PROPS) { styles[key] = computed.getPropertyValue(cssProp); } styles.className = className; let inferredDirection = styles.flexDirection || "row"; if (!isFlex && hasLayoutChildren) { const rects = children.map(child => child.getBoundingClientRect()); const sameRowCount = rects.filter(childRect => Math.abs(childRect.top - rects[0].top) < 6).length; inferredDirection = sameRowCount >= Math.max(2, children.length * 0.65) ? "row" : "column"; } const childRects = children.map(child => { const childStyle = window.getComputedStyle(child); return absoluteRect(child.getBoundingClientRect(), childStyle); }); const inferredGrid = inferGridMetrics(childRects, parsePixels(styles.columnGap || styles.gap)); const gridLike = isGridLikeLayout({ isGrid, isFlex, isTable, semanticLayout, styles, childRects, inferredGrid }); const layoutType = gridLike ? "grid" : isFlex ? "flex" : isTable ? "table" : "inferred"; return { type: layoutType, display, direction: inferredDirection, wrap: styles.flexWrap, justifyContent: styles.justifyContent, alignItems: styles.alignItems, alignContent: styles.alignContent, gap: parsePixels(styles.gap), rowGap: parsePixels(styles.rowGap), columnGap: parsePixels(styles.columnGap), padding: { top: parsePixels(styles.paddingTop), right: parsePixels(styles.paddingRight), bottom: parsePixels(styles.paddingBottom), left: parsePixels(styles.paddingLeft) }, margin: { top: parsePixels(styles.marginTop), right: parsePixels(styles.marginRight), bottom: parsePixels(styles.marginBottom), left: parsePixels(styles.marginLeft) }, grid: gridLike ? { columns: styles.gridTemplateColumns, rows: styles.gridTemplateRows, autoFlow: styles.gridAutoFlow, columnCount: inferredGrid.columnCount, rowCount: inferredGrid.rowCount } : null, inferredGrid, sizing: { width: styles.width, height: styles.height, minWidth: styles.minWidth, maxWidth: styles.maxWidth, minHeight: styles.minHeight, maxHeight: styles.maxHeight }, childCount: children.length, childRects, confidence: isFlex || isGrid ? 1 : gridLike ? 0.86 : semanticLayout ? 0.72 : 0.55, rect: { width: round(rect.width), height: round(rect.height) } }; } function collectAttributes(element) { const attrs = {}; for (const attr of Array.from(element.attributes || [])) { if ( attr.name === "id" || attr.name === "class" || attr.name === "alt" || attr.name === "title" || attr.name === "href" || attr.name === "src" || attr.name === "role" || attr.name.startsWith("aria-") ) { attrs[attr.name] = attr.value; } } return attrs; } function extractBackgroundUrls(value) { if (!value || value === "none") return []; const urls = []; const re = /url\((["']?)(.*?)\1\)/g; let match; while ((match = re.exec(value))) { if (!match[2] || match[2] === "undefined" || match[2] === "null") continue; try { const url = new URL(match[2], document.baseURI).href; if (!/\/undefined(?:$|[?#])/.test(url)) urls.push(url); } catch { urls.push(match[2]); } } return urls; } function extractStyleImageUrls(styles) { return [ ...extractBackgroundUrls(styles.backgroundImage), ...extractBackgroundUrls(styles.maskImage), ...extractBackgroundUrls(styles.webkitMaskImage) ]; } function parseSrcset(value) { if (!value) return []; return String(value) .split(",") .map(item => item.trim().split(/\s+/)[0]) .filter(Boolean); } function splitConcatenatedUrls(value) { if (!value) return []; const text = String(value).trim(); if (!text) return []; const matches = text.match(/https?:\/\/(?:(?!https?:\/\/).)+/gi); if (matches && matches.length > 1) return matches; return [text]; } function canUseDescendantSources(element) { return /^(PICTURE|VIDEO|AUDIO)$/i.test(element.tagName); } function getElementImageCandidates(element) { const candidates = []; const add = value => { for (const item of splitConcatenatedUrls(value)) { const normalized = normalizeAssetUrl(item); if (normalized) candidates.push(normalized); } }; add(element.currentSrc); add(element.src); add(element.poster); add(element.getAttribute?.("src")); add(element.getAttribute?.("data-src")); add(element.getAttribute?.("data-original")); add(element.getAttribute?.("data-lazy")); add(element.getAttribute?.("data-lazy-src")); add(element.getAttribute?.("data-bg")); add(element.getAttribute?.("data-background")); for (const value of parseSrcset(element.srcset || element.getAttribute?.("srcset"))) { add(value); } if (canUseDescendantSources(element)) { for (const source of Array.from(element.querySelectorAll?.("source") || [])) { for (const value of parseSrcset(source.srcset || source.getAttribute("srcset"))) { add(value); } add(source.src); } } return Array.from(new Set(candidates)); } function normalizeText(value) { return String(value || "").replace(/\s+/g, "").trim(); } function overlapRatio(a, b) { const intersection = intersectRects(a, b); if (!intersection) return 0; const area = Math.max(1, Math.min( (a.width || 1) * (a.height || 1), (b.width || 1) * (b.height || 1) )); return (intersection.width * intersection.height) / area; } function hasMaskImage(styles) { return Boolean( (styles.maskImage && styles.maskImage !== "none") || (styles.webkitMaskImage && styles.webkitMaskImage !== "none") ); } function hasUsableBackgroundImage(styles) { return Boolean(styles.backgroundImage && styles.backgroundImage !== "none"); } function hasVisiblePaintColor(value) { return Boolean(value && value !== "transparent" && !/rgba\([^)]*,\s*0(?:\.0+)?\)/i.test(value)); } function isLightOpaquePaint(value) { if (!value || value === "transparent") return false; const rgba = String(value).match(/rgba?\(([^)]+)\)/i); if (rgba) { const parts = rgba[1].split(",").map(part => parseFloat(part.trim())); const alpha = parts[3] == null ? 1 : parts[3]; return alpha > 0.92 && parts[0] >= 235 && parts[1] >= 235 && parts[2] >= 235; } const hex = String(value).trim().match(/^#([0-9a-f]{3}|[0-9a-f]{6})$/i); if (!hex) return false; const body = hex[1].length === 3 ? hex[1].split("").map(char => char + char).join("") : hex[1]; return parseInt(body.slice(0, 2), 16) >= 235 && parseInt(body.slice(2, 4), 16) >= 235 && parseInt(body.slice(4, 6), 16) >= 235; } function getMaxBorderWidth(styles) { return Math.max( parsePixels(styles.borderTopWidth), parsePixels(styles.borderRightWidth), parsePixels(styles.borderBottomWidth), parsePixels(styles.borderLeftWidth) ); } function hasCornerRadius(styles) { return Boolean( parsePixels(styles.borderTopLeftRadius) || parsePixels(styles.borderTopRightRadius) || parsePixels(styles.borderBottomRightRadius) || parsePixels(styles.borderBottomLeftRadius) ); } function classifyVisualRole(element, styles, children, currentSrc, backgroundUrls, layout) { const tag = element?.tagName || ""; if (/^(IMG|PICTURE|VIDEO|CANVAS|SOURCE)$/i.test(tag) || currentSrc || backgroundUrls?.length) { return "image-node"; } const hasDecor = getMaxBorderWidth(styles) > 0 || hasCornerRadius(styles) || (styles.boxShadow && styles.boxShadow !== "none") || (styles.filter && /drop-shadow/i.test(styles.filter)); const isContainer = /^(HTML|BODY|DIV|SECTION|MAIN|ARTICLE|HEADER|NAV|FOOTER|ASIDE|UL|OL|LI|FORM)$/i.test(tag); if ( (layout || children?.length) && isContainer && !hasUsableBackgroundImage(styles) && !hasDecor && (!hasVisiblePaintColor(styles.backgroundColor) || isLightOpaquePaint(styles.backgroundColor)) ) { return "layout-wrapper"; } if ( hasVisiblePaintColor(styles.backgroundColor) || hasUsableBackgroundImage(styles) || hasDecor ) { return "paint-node"; } if (layout || children?.length) return "layout-wrapper"; return "paint-node"; } function shouldSkipPseudoBackground(styles, parentRect) { if (!styles.backgroundImage || styles.backgroundImage === "none") return false; if (styles.backgroundRepeat && styles.backgroundRepeat !== "no-repeat") return true; if (/auto\s+\d+px/i.test(styles.backgroundSize || "")) return true; if (/-\d+(?:\.\d+)?px/.test(styles.backgroundPosition || "")) return true; const parentArea = Math.max(1, (parentRect?.width || 1) * (parentRect?.height || 1)); if (parentArea > 12000 && /(?:^|\s)(?:\d+px\s+auto|auto\s+\d+px|auto)(?:\s|$)/i.test(styles.backgroundSize || "")) return true; return false; } function hasMeaningfulElementChildren(element) { return Array.from(element.children || []).some(child => { const rect = child.getBoundingClientRect(); const style = window.getComputedStyle(child); return rect.width > 0 && rect.height > 0 && style.display !== "none" && style.visibility !== "hidden" && Number(style.opacity || 1) > 0; }); } function hasWhiteTextDescendant(element) { const walker = document.createTreeWalker(element, NodeFilter.SHOW_TEXT); let textNode = walker.nextNode(); while (textNode) { if (textNode.textContent.trim()) { const parent = textNode.parentElement; if (parent) { const color = window.getComputedStyle(parent).color; if (/rgb\(255,\s*255,\s*255\)|rgba\(255,\s*255,\s*255,\s*(?:0\.\d+|1)\)/i.test(color)) { return true; } } } textNode = walker.nextNode(); } return false; } function visibleTextLength(element) { return normalizeText(element.innerText).length; } function shouldSkipTextNode(textNode, parentStyle) { const parent = textNode.parentElement; if (!parent) return true; if (parent.closest("[hidden], [aria-hidden='true']")) return true; if (parentStyle.display === "none" || parentStyle.visibility === "hidden") return true; if (Number(parentStyle.opacity || 1) < 0.04) return true; if (parseFloat(parentStyle.fontSize || "0") <= 0) return true; const color = parentStyle.color || ""; if (/rgba\([^)]*,\s*0(?:\.0+)?\)/i.test(color)) return true; return false; } function pushCaptureChild(children, childNode) { if (!childNode) return; if (childNode.type === "TEXT") { const text = normalizeText(childNode.text); if (!text) return; const duplicate = children.some(existing => { if (existing.type !== "TEXT") return false; if (normalizeText(existing.text) !== text) return false; return overlapRatio(existing.visibleRect || existing.rect, childNode.visibleRect || childNode.rect) > 0.72; }); if (duplicate) return; } children.push(childNode); } function isUsefulHeaderElement(element, headerRect) { if (!element || isWebToPPTElement(element)) return false; if (/^(HTML|BODY|SCRIPT|STYLE|META|LINK)$/i.test(element.tagName || "")) return false; const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); if (!isVisible(element, rect, style)) return false; const absRect = absoluteRect(rect, style); const intersection = intersectRects(absRect, headerRect); if (!intersection) return false; const overlap = (intersection.width * intersection.height) / Math.max(1, absRect.width * absRect.height); if (overlap < 0.55) return false; if (absRect.width > headerRect.width * 0.92 && absRect.height > headerRect.height * 0.8) return false; const text = normalizeText(element.innerText || element.textContent || element.getAttribute?.("aria-label") || element.getAttribute?.("alt") || element.getAttribute?.("placeholder") || ""); const name = `${element.tagName || ""} ${element.id || ""} ${element.className || ""} ${element.getAttribute?.("role") || ""}`; const hasMedia = /^(IMG|SVG|PICTURE|CANVAS)$/i.test(element.tagName || "") || Boolean(element.querySelector?.("img, svg, picture, canvas")); const isControl = /^(A|BUTTON|INPUT|SELECT|TEXTAREA|LABEL)$/i.test(element.tagName || "") || /button|btn|link|logo|icon|search|input|nav|menu|item|login|register/i.test(String(name)); return Boolean(text || hasMedia || isControl); } function collectHeaderPointElements(headerRect) { const found = new Set(); const xSteps = Math.max(8, Math.min(28, Math.ceil(headerRect.width / 88))); const ySteps = Math.max(2, Math.min(6, Math.ceil(headerRect.height / 24))); for (let yIndex = 0; yIndex <= ySteps; yIndex += 1) { const y = Math.min(window.innerHeight - 1, Math.max(0, headerRect.y + (headerRect.height * yIndex / Math.max(1, ySteps)))); for (let xIndex = 0; xIndex <= xSteps; xIndex += 1) { const x = Math.min(window.innerWidth - 1, Math.max(0, headerRect.x + (headerRect.width * xIndex / Math.max(1, xSteps)))); for (const element of document.elementsFromPoint(x, y) || []) { if (isUsefulHeaderElement(element, headerRect)) { found.add(element); } } } } return Array.from(found) .filter(element => { return !Array.from(found).some(other => other !== element && other.contains(element) && isUsefulHeaderElement(other, headerRect)); }) .sort((a, b) => { const ar = a.getBoundingClientRect(); const br = b.getBoundingClientRect(); if (Math.abs(ar.top - br.top) > 4) return ar.top - br.top; return ar.left - br.left; }); } function isProbablyComplexCard(element, absRect, styles) { if (element.tagName === "HTML" || element.tagName === "BODY") return false; if (absRect.y < window.innerHeight * 2.2) return false; if (absRect.width < 240 || absRect.width > Math.min(window.innerWidth * 0.75, 980)) return false; if (absRect.height < 150 || absRect.height > 640) return false; const imageUrls = extractStyleImageUrls(styles); const hasImgChild = Boolean(element.querySelector("img, picture, video")); const hasWhiteText = hasWhiteTextDescendant(element); const hasRoundedCard = parseFloat(styles.borderTopLeftRadius || "0") > 0 || parseFloat(styles.borderTopRightRadius || "0") > 0 || parseFloat(styles.borderBottomLeftRadius || "0") > 0 || parseFloat(styles.borderBottomRightRadius || "0") > 0; const hasCardClass = /card|case|customer|client|solution|scene|cert|safe|security|item|slide|swiper/i.test(String(element.className || "")); const hasVisualFill = imageUrls.length > 0 || (styles.backgroundColor && !/rgba\(0,\s*0,\s*0,\s*0\)|transparent/i.test(styles.backgroundColor)); if (visibleTextLength(element) < 8 && !hasImgChild && !imageUrls.length && !hasCardClass) return false; return hasWhiteText || hasImgChild || (hasVisualFill && (hasRoundedCard || hasCardClass)); } function addTargetIfNonOverlapping(targets, target, maxOverlapRatio = 0.65) { const area = Math.max(1, target.rect.width * target.rect.height); const overlaps = targets.some(item => { const intersection = intersectRects(item.rect, target.rect); if (!intersection) return false; const intersectionArea = intersection.width * intersection.height; return intersectionArea / area > maxOverlapRatio; }); if (!overlaps) targets.push(target); } function clippedViewportRect(absRect, minHeight = 1) { if (!absRect) return null; const rect = intersectRects(absRect, { x: 0, y: absRect.y, width: window.innerWidth, height: Math.min(absRect.height, window.innerHeight) }); if (!rect || rect.height < minHeight || rect.width < 1) return null; return rect; } function getSelectionElement(options = {}) { if (!options.selectionId) return null; try { const raw = String(options.selectionId); const escaped = window.CSS?.escape ? CSS.escape(raw) : raw.replace(/["\\]/g, "\\$&"); return document.querySelector(`[data-web-to-ppt-selection-id="${escaped}"]`); } catch { return null; } } function getSelectionRect(element) { if (!element) return null; const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); if (!isVisible(element, rect, style)) return null; return absoluteRect(rect, style); } function textNodeToCaptureNode(textNode, clipRect, context = {}) { const text = textNode.textContent.replace(/\s+/g, " ").trim(); if (!text) return null; const range = document.createRange(); range.selectNodeContents(textNode); const rect = range.getBoundingClientRect(); range.detach(); const parentStyle = window.getComputedStyle(textNode.parentElement); if (shouldSkipTextNode(textNode, parentStyle)) return null; const absRect = absoluteRect(rect, parentStyle); const visibleRect = intersectWithClip(absRect, clipRect); if (!visibleRect || visibleRect.width <= 0 || visibleRect.height <= 0) return null; return { id: `text-${++nodeCounter}`, type: "TEXT", tag: "#text", name: text.slice(0, 40), text, rect: absRect, visibleRect, clipped: visibleRect.width !== absRect.width || visibleRect.height !== absRect.height, visualRole: "text-node", section: context.section || "content", ...layerMetaFor("TEXT"), styles: extractStyles(parentStyle), attributes: {}, children: [] }; } function elementLabelToTextNode(element, clipRect, context = {}) { const text = normalizeText( element.getAttribute?.("aria-label") || element.getAttribute?.("alt") || element.getAttribute?.("title") || element.getAttribute?.("placeholder") || element.value || "" ); if (!text) return null; const style = window.getComputedStyle(element); const rect = element.getBoundingClientRect(); if (!isVisible(element, rect, style)) return null; const absRect = absoluteRect(rect, style); const visibleRect = intersectWithClip(absRect, clipRect); if (!visibleRect) return null; return { id: `text-${++nodeCounter}`, type: "TEXT", tag: "#text", name: text.slice(0, 40), text, rect: absRect, visibleRect, clipped: visibleRect.width !== absRect.width || visibleRect.height !== absRect.height, visualRole: "text-node", section: context.section || "content", ...layerMetaFor("TEXT"), styles: extractStyles(style), attributes: { "data-generated-from": "element-label" }, children: [] }; } function pseudoElementToCaptureNode(element, pseudo, parentRect, assetUrls, clipRect, context = {}) { const computed = window.getComputedStyle(element, pseudo); if (!computed || computed.display === "none" || computed.visibility === "hidden") return null; const styles = extractStyles(computed); const rawContent = computed.getPropertyValue("content"); const text = rawContent && rawContent !== "none" && rawContent !== "normal" ? rawContent.replace(/^["']|["']$/g, "") : ""; const backgroundUrls = extractStyleImageUrls(styles); if (text && normalizeText(element.innerText).includes(normalizeText(text))) { return null; } if (!text && !backgroundUrls.length) { return null; } if (!text && hasMaskImage(styles) && !hasUsableBackgroundImage(styles) && parentRect.width * parentRect.height > 4096) { return null; } if (!text && backgroundUrls.length && shouldSkipPseudoBackground(styles, parentRect)) { return null; } for (const url of backgroundUrls) { assetUrls.add(url); } const hasVisibleBackground = ( styles.backgroundColor && styles.backgroundColor !== "rgba(0, 0, 0, 0)" && styles.backgroundColor !== "transparent" ) || backgroundUrls.length; if (!text && !hasVisibleBackground) return null; const absRect = absoluteRect(parentRect, styles); const visibleRect = intersectWithClip(absRect, clipRect); if (!visibleRect) return null; return { id: `pseudo-${++nodeCounter}`, type: text ? "TEXT" : "RECTANGLE", tag: pseudo, name: pseudo, text, rect: absRect, visibleRect, clipped: visibleRect.width !== absRect.width || visibleRect.height !== absRect.height, visualRole: text ? "text-node" : "paint-node", section: context.section || "content", ...layerMetaFor(text ? "TEXT" : "RECTANGLE"), styles, attributes: {}, backgroundImages: backgroundUrls, children: [] }; } function elementToCaptureNode(element, assetUrls, clipRect, diagnostics, context = {}) { if (!element || SKIP_TAGS.has(element.tagName)) return null; if (isWebToPPTElement(element)) return null; if (isCarouselClone(element)) { if (diagnostics) diagnostics.skippedCarouselClones += 1; return null; } const computed = window.getComputedStyle(element); const clientRect = element.getBoundingClientRect(); if (!isVisible(element, clientRect, computed)) return null; const styles = extractStyles(computed); const absRect = element.tagName === "BODY" || element.tagName === "HTML" ? getDocumentRect() : absoluteRect(clientRect, styles); const section = element.tagName === "HTML" || element.tagName === "BODY" ? "document" : inferSection(element, absRect, styles, context.section); const childContext = { section }; const layout = extractLayout(computed, element, absRect); const documentClip = clipRect || getDocumentRect(); const visibleRect = intersectRects(absRect, documentClip); if (!visibleRect) { if (diagnostics) diagnostics.skippedOutsideClip += 1; return null; } const ownClip = clipsChildren(styles) || element.tagName === "BODY" || element.tagName === "HTML" ? intersectRects(documentClip, absRect) || visibleRect : documentClip; const renderRect = shouldClampToClip(element, absRect, visibleRect) ? visibleRect : absRect; const attrs = collectAttributes(element); const children = []; const imageCandidates = getElementImageCandidates(element); const currentSrc = imageCandidates[0] || null; let backgroundUrls = extractStyleImageUrls(styles); if (/^(IMG|VIDEO|SOURCE|PICTURE)$/i.test(element.tagName)) { for (const url of imageCandidates) { assetUrls.add(url); } } for (const url of backgroundUrls) { assetUrls.add(url); } const beforeNode = pseudoElementToCaptureNode(element, "::before", clientRect, assetUrls, ownClip, childContext); pushCaptureChild(children, beforeNode); for (const child of getChildNodes(element)) { if (child.nodeType === Node.TEXT_NODE) { const textNode = textNodeToCaptureNode(child, ownClip, childContext); pushCaptureChild(children, textNode); } else if (child.nodeType === Node.ELEMENT_NODE) { const childNode = elementToCaptureNode(child, assetUrls, ownClip, diagnostics, childContext); pushCaptureChild(children, childNode); } } if (!children.some(child => child.type === "TEXT")) { pushCaptureChild(children, elementLabelToTextNode(element, ownClip, childContext)); } if (isHeaderLikeElement(element) && children.length === 0) { for (const headerElement of collectHeaderPointElements(absRect)) { if (headerElement === element || element.contains(headerElement)) continue; const childNode = elementToCaptureNode(headerElement, assetUrls, ownClip, diagnostics, { section: "header" }); pushCaptureChild(children, childNode); } } const afterNode = pseudoElementToCaptureNode(element, "::after", clientRect, assetUrls, ownClip, childContext); pushCaptureChild(children, afterNode); // SVG → 序列化为 data URL 图片 if ((element.tagName === "SVG" || element.tagName === "svg") && !backgroundUrls.length) { try { const svgClone = element.cloneNode(true); if (!svgClone.getAttribute('xmlns')) svgClone.setAttribute('xmlns', 'http://www.w3.org/2000/svg'); const svgStr = new XMLSerializer().serializeToString(svgClone); const svgDataUrl = 'data:image/svg+xml;base64,' + btoa(unescape(encodeURIComponent(svgStr))); backgroundUrls = [svgDataUrl]; console.log('[WebToPPT] SVG captured:', (element.className || element.id || '').slice(0, 30), 'size:', svgStr.length); } catch (e) { console.warn('[WebToPPT] SVG capture failed:', e.message, element.tagName, (element.className || '').slice(0, 30)); } } const type = element.tagName === "IMG" || backgroundUrls.length ? "RECTANGLE" : "FRAME"; return { id: `node-${++nodeCounter}`, type, tag: element.tagName, name: attrs.id || attrs.class || element.tagName.toLowerCase(), rect: renderRect, sourceRect: absRect, visibleRect, clipRect: ownClip, clipped: renderRect.width !== absRect.width || renderRect.height !== absRect.height, visualRole: classifyVisualRole(element, styles, children, currentSrc, backgroundUrls, layout), section, ...layerMetaFor(type), styles, layout, attributes: attrs, src: currentSrc, backgroundImages: backgroundUrls, children }; } function normalizeAssetUrl(url) { if (!url || url === "undefined" || url === "null") return null; try { const absolute = new URL(url, document.baseURI).href; if (/\/undefined(?:$|[?#])/.test(absolute)) return null; return absolute; } catch { return null; } } function toDataUrl(contentType, base64) { return `data:${contentType || "application/octet-stream"};base64,${base64}`; } function blobToDataUrl(blob) { return new Promise((resolve, reject) => { const reader = new FileReader(); reader.onload = () => resolve(reader.result); reader.onerror = reject; reader.readAsDataURL(blob); }); } async function directFetchAsset(url) { const response = await fetch(url, { credentials: "include", cache: "force-cache" }); if (!response.ok) throw new Error(`HTTP ${response.status}`); const blob = await response.blob(); return { ok: true, mimeType: blob.type || response.headers.get("content-type") || "application/octet-stream", data: await blobToDataUrl(blob) }; } function captureVideoPoster(url) { const video = Array.from(document.querySelectorAll("video")).find(item => { const source = normalizeAssetUrl(item.currentSrc || item.src || item.querySelector("source")?.src); return source === url; }); if (!video || !video.videoWidth || !video.videoHeight) return null; const canvas = document.createElement("canvas"); canvas.width = video.videoWidth; canvas.height = video.videoHeight; const ctx = canvas.getContext("2d"); ctx.drawImage(video, 0, 0, canvas.width, canvas.height); return canvasToPng(canvas); } async function proxyFetchAsset(url) { if (!chrome?.runtime?.sendMessage) { throw new Error("扩展代理不可用"); } const response = await chrome.runtime.sendMessage({ type: "WEB_TO_PPT_FETCH_ASSET", url }); if (!response?.ok) { throw new Error(response?.error || "代理拉取失败"); } return { ok: true, mimeType: response.contentType || "application/octet-stream", data: toDataUrl(response.contentType, response.base64) }; } async function captureVisibleTabImage() { if (!chrome?.runtime?.sendMessage) { return null; } let response = null; try { response = await chrome.runtime.sendMessage({ type: "WEB_TO_PPT_VISIBLE_TAB" }); } catch (error) { console.warn("[Web to PPT] Raster fallback message failed", error); return null; } if (!response?.ok || !response.dataUrl) { console.warn("[Web to PPT] Raster fallback skipped", response?.error || "标签页截图失败"); return null; } const image = new Image(); image.decoding = "async"; image.src = response.dataUrl; try { await new Promise((resolve, reject) => { image.onload = resolve; image.onerror = reject; }); return image; } catch (error) { console.warn("[Web to PPT] Raster fallback image decode failed", error); return null; } } async function cropViewportRegion(rect, scrollY) { const image = await captureVisibleTabImage(); if (!image) return null; const ratioX = image.naturalWidth / Math.max(1, window.innerWidth); const ratioY = image.naturalHeight / Math.max(1, window.innerHeight); const sourceX = Math.max(0, Math.round(rect.x * ratioX)); const sourceY = Math.max(0, Math.round((rect.y - scrollY) * ratioY)); const sourceWidth = Math.min(image.naturalWidth - sourceX, Math.round(rect.width * ratioX)); const sourceHeight = Math.min(image.naturalHeight - sourceY, Math.round(rect.height * ratioY)); if (sourceWidth <= 0 || sourceHeight <= 0) return null; const canvas = document.createElement("canvas"); canvas.width = Math.max(1, Math.round(rect.width)); canvas.height = Math.max(1, Math.round(rect.height)); const ctx = canvas.getContext("2d"); ctx.drawImage(image, sourceX, sourceY, sourceWidth, sourceHeight, 0, 0, canvas.width, canvas.height); return canvasToPng(canvas); } function hideFixedTopOverlays() { const hidden = []; for (const element of Array.from(document.body?.querySelectorAll("*") || [])) { if (isWebToPPTElement(element)) continue; const style = window.getComputedStyle(element); if (style.position !== "fixed" && style.position !== "sticky") continue; const rect = element.getBoundingClientRect(); if (rect.width < 8 || rect.height < 8) continue; const topBar = rect.width >= window.innerWidth * 0.25 && rect.height >= 20 && rect.height <= 180 && rect.top <= 140 && rect.bottom >= 0; const sideFloat = rect.right >= window.innerWidth - 160 && rect.width <= 180 && rect.height >= 40; const bottomFloat = rect.bottom >= window.innerHeight - 120 && rect.width <= Math.max(720, window.innerWidth * 0.55) && rect.height <= 180; if (!topBar && !sideFloat && !bottomFloat) continue; const previous = { element, value: element.style.getPropertyValue("visibility"), priority: element.style.getPropertyPriority("visibility") }; element.style.setProperty("visibility", "hidden", "important"); hidden.push(previous); } return () => { for (const item of hidden) { if (item.value) { item.element.style.setProperty("visibility", item.value, item.priority || ""); } else { item.element.style.removeProperty("visibility"); } } }; } function hideWebToPPTOverlays() { const hidden = []; for (const selector of [ "#__web_to_ppt_panel_root__", "#__web_to_ppt_picker_root__", "#__web_to_ppt_picker_box__", "#__web_to_ppt_picker_label__" ]) { const element = document.querySelector(selector); if (!element) continue; hidden.push({ element, visibility: element.style.getPropertyValue("visibility"), visibilityPriority: element.style.getPropertyPriority("visibility"), display: element.style.getPropertyValue("display"), displayPriority: element.style.getPropertyPriority("display"), opacity: element.style.getPropertyValue("opacity"), opacityPriority: element.style.getPropertyPriority("opacity") }); element.style.setProperty("visibility", "hidden", "important"); element.style.setProperty("opacity", "0", "important"); element.style.setProperty("display", "none", "important"); } return () => { for (const item of hidden) { if (item.visibility) { item.element.style.setProperty("visibility", item.visibility, item.visibilityPriority || ""); } else { item.element.style.removeProperty("visibility"); } if (item.display) { item.element.style.setProperty("display", item.display, item.displayPriority || ""); } else { item.element.style.removeProperty("display"); } if (item.opacity) { item.element.style.setProperty("opacity", item.opacity, item.opacityPriority || ""); } else { item.element.style.removeProperty("opacity"); } } }; } function hideTextForBackgroundRaster(targetRect) { const style = document.createElement("style"); style.setAttribute("data-web-to-ppt-background-raster", "true"); style.textContent = ` html body *:not(#__web_to_ppt_panel_root__):not(#__web_to_ppt_picker_root__) { color: transparent !important; -webkit-text-fill-color: transparent !important; text-shadow: none !important; caret-color: transparent !important; } html body *::before, html body *::after, html body input::placeholder, html body textarea::placeholder { color: transparent !important; -webkit-text-fill-color: transparent !important; text-shadow: none !important; } `; document.documentElement.appendChild(style); return () => style.remove(); } function hideHeroForegroundForRaster(targetRect) { const hidden = []; const targetArea = Math.max(1, targetRect.width * targetRect.height); const isTransparent = value => !value || /transparent|rgba\([^)]*,\s*0(?:\.0+)?\)/i.test(value); for (const element of Array.from(document.body?.querySelectorAll("*") || [])) { if (isWebToPPTElement(element)) continue; const style = window.getComputedStyle(element); const rect = element.getBoundingClientRect(); if (!isVisible(element, rect, style)) continue; const absRect = absoluteRect(rect, style); const intersection = intersectRects(absRect, targetRect); if (!intersection) continue; const tag = element.tagName || ""; const areaRatio = (absRect.width * absRect.height) / targetArea; const text = normalizeText(element.innerText || element.textContent || ""); const name = `${tag} ${element.id || ""} ${element.className || ""} ${element.getAttribute?.("role") || ""}`; const hasFill = !isTransparent(style.backgroundColor) || (style.backgroundImage && style.backgroundImage !== "none") || parseFloat(style.borderTopWidth || "0") > 0 || parseFloat(style.borderRightWidth || "0") > 0 || parseFloat(style.borderBottomWidth || "0") > 0 || parseFloat(style.borderLeftWidth || "0") > 0 || (style.boxShadow && style.boxShadow !== "none"); const isMedia = /^(IMG|PICTURE|VIDEO|CANVAS)$/i.test(tag); const isLargeMedia = isMedia && areaRatio > 0.08 && absRect.width > 260 && absRect.height > 140; const isControl = /^(A|BUTTON|INPUT|SELECT|TEXTAREA|LABEL|SVG|I|EM)$/i.test(tag) || /button|btn|link|nav|menu|tab|card|coupon|activity|hot|item|tag|badge|icon|service|float|customer|consult|sidebar/i.test(String(name)); const isForegroundBlock = areaRatio < 0.34 && ( isControl || (text.length > 0 && areaRatio < 0.24) || (hasFill && areaRatio < 0.24) ); if (!isForegroundBlock || isLargeMedia) continue; hidden.push({ element, visibility: element.style.getPropertyValue("visibility"), visibilityPriority: element.style.getPropertyPriority("visibility") }); element.style.setProperty("visibility", "hidden", "important"); } return () => { for (const item of hidden) { if (item.visibility) { item.element.style.setProperty("visibility", item.visibility, item.visibilityPriority || ""); } else { item.element.style.removeProperty("visibility"); } } }; } function waitForPaint(ms = 80) { return new Promise(resolve => { requestAnimationFrame(() => { requestAnimationFrame(() => { window.setTimeout(resolve, ms); }); }); }); } function hidePageScrollbars() { const style = document.createElement("style"); style.textContent = ` html, body { scrollbar-width: none !important; -ms-overflow-style: none !important; } html::-webkit-scrollbar, body::-webkit-scrollbar, *::-webkit-scrollbar { width: 0 !important; height: 0 !important; display: none !important; } `; document.documentElement.appendChild(style); return () => style.remove(); } function isLikelyHeroOrCarousel(element, absRect, styles) { const name = `${element.id || ""} ${element.className || ""} ${element.getAttribute?.("role") || ""}`; const isNamed = /hero|banner|kv|carousel|swiper|slick|slider|slide|campaign|masthead/i.test(String(name)); const isMedia = /^(PICTURE|VIDEO|CANVAS)$/i.test(element.tagName) || Boolean(element.querySelector?.("picture, video, canvas")); const nearTop = absRect.y < Math.max(window.innerHeight * 1.4, 900); const wide = absRect.width >= window.innerWidth * 0.65; const tall = absRect.height >= 180; const visual = isMedia || extractStyleImageUrls(styles).length > 0 || /gradient|url\(/i.test(styles.backgroundImage || ""); return nearTop && wide && tall && (isNamed || visual); } function isLikelyFullWidthVisualSection(element, absRect, styles) { if (element.tagName === "HTML" || element.tagName === "BODY") return false; const name = `${element.id || ""} ${element.className || ""}`; const named = /banner|hero|section|panel|visual|picture|media|video|campaign|activity|recommend|promo|feature/i.test(String(name)); const media = /^(PICTURE|VIDEO|CANVAS)$/i.test(element.tagName) || Boolean(element.querySelector?.("picture, video, canvas")); const hasImage = extractStyleImageUrls(styles).length > 0 || Boolean(element.querySelector?.("img, picture, video, canvas")); const wide = absRect.width >= window.innerWidth * 0.78; const tall = absRect.height >= 220; const saneHeight = absRect.height <= window.innerHeight * 1.05; return wide && tall && saneHeight && (media || hasImage || named); } function addRasterTarget(targets, target, overlap = 0.7) { if (!target?.rect) return; if (target.rect.width < 2 || target.rect.height < 2) return; const viewportRect = { x: 0, y: target.rect.y, width: window.innerWidth, height: Math.min(target.rect.height, window.innerHeight) }; if (!intersectRects(target.rect, viewportRect)) return; addTargetIfNonOverlapping(targets, target, overlap); } function addRasterTargetSlices(targets, target, overlap = 0.7) { if (!target?.rect) return; const maxHeight = Math.max(320, Math.floor(window.innerHeight * 0.86)); const totalHeight = Math.max(1, target.rect.height); if (totalHeight <= maxHeight) { addRasterTarget(targets, target, overlap); return; } let offset = 0; let index = 1; while (offset < totalHeight - 1) { const remaining = totalHeight - offset; const height = Math.min(maxHeight, remaining); addRasterTarget(targets, { ...target, name: `${target.name} ${index}`, rect: { x: target.rect.x, y: round(target.rect.y + offset), width: target.rect.width, height: round(height) } }, overlap); offset += height; index += 1; } } function countKeyword(text, pattern) { return (String(text || "").match(pattern) || []).length; } function isLikelyCommerceSection(element, absRect) { if (!element || element.tagName === "HTML" || element.tagName === "BODY") return false; if (absRect.y < 240) return false; if (absRect.width < window.innerWidth * 0.62) return false; if (absRect.height < 64 || absRect.height > getDocumentRect().height * 0.85) return false; const name = `${element.id || ""} ${element.className || ""}`.trim(); if (/^app$/i.test(name)) return false; const text = normalizeText(element.innerText || ""); const actionCount = countKeyword(text, /立即购买|立即开通|立即领取|免费体验|了解详情/g); const cardCount = countKeyword(text, /云服务器|云数据库|对象存储|CDN|LSS|OCR|API|价格|配置/g); const named = /module|product|coupon|goods|package|card|recommend|activity|campaign|promo|experience|权益|优惠|more|hot/i.test(name); const contentNamed = /新用户|上云|产品|特惠|优惠|活动|推荐|更多产品|企业实名|价格|立即/g.test(text); return named && contentNamed || actionCount >= 3 || cardCount >= 4; } function getRasterFallbackTargets(options = {}) { const targets = []; const mixedMode = options.captureMode !== "editable"; const selectionElement = getSelectionElement(options); const selectionRect = getSelectionRect(selectionElement); if (selectionRect) { addRasterTarget(targets, { name: "Selected element raster fallback", rect: clippedViewportRect(selectionRect, 1) || selectionRect, mode: "visual" }, 1); return targets; } for (const element of Array.from(document.querySelectorAll("header, nav, [role='navigation'], [class*='header' i], [class*='nav' i]"))) { const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); if (!isVisible(element, rect, style)) continue; const absRect = absoluteRect(rect, style); const nearTop = absRect.y <= 120 || style.position === "fixed" || style.position === "sticky"; const likelyHeader = rect.height >= 24 && rect.height <= 160 && rect.width >= window.innerWidth * 0.45; if (!nearTop || !likelyHeader) continue; addRasterTarget(targets, { name: "Header raster fallback", rect: absRect, mode: "visual" }, 0.8); break; } if (mixedMode) { const heroCandidates = Array.from(document.querySelectorAll([ "section", "main > div", "[role='banner']", "[class*='hero' i]", "[class*='banner' i]", "[class*='kv' i]", "[class*='carousel' i]", "[class*='swiper' i]", "[class*='slick' i]", "[class*='slider' i]", "[class*='slide' i]", "[class*='campaign' i]", "picture", "video", "canvas" ].join(","))); const heroElements = new Set(); for (const element of heroCandidates) { const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); if (!isVisible(element, rect, style)) continue; const absRect = absoluteRect(rect, style); const styles = extractStyles(style); if (!isLikelyHeroOrCarousel(element, absRect, styles)) continue; if (heroElements.has(element)) continue; heroElements.add(element); const targetRect = clippedViewportRect(absRect, 160); if (!targetRect) continue; addRasterTarget(targets, { name: "Hero carousel raster fallback", rect: targetRect, mode: "hero-background" }, 0.45); if (heroElements.size >= 3) break; } const commerceCandidates = Array.from(document.querySelectorAll([ "section", "main > div", "[class*='module' i]", "[class*='product' i]", "[class*='coupon' i]", "[class*='goods' i]", "[class*='package' i]", "[class*='recommend' i]", "[class*='activity' i]", "[class*='campaign' i]", "[class*='promo' i]", "[class*='more' i]", "[class*='experience' i]" ].join(","))); const commerceTargets = []; for (const element of commerceCandidates) { if (commerceTargets.length >= 12) break; const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); if (!isVisible(element, rect, style)) continue; const absRect = absoluteRect(rect, style); if (!isLikelyCommerceSection(element, absRect)) continue; addRasterTargetSlices(commerceTargets, { name: "Commerce section raster fallback", mode: "background", rect: { x: Math.max(0, absRect.x), y: absRect.y, width: Math.min(window.innerWidth, absRect.width), height: absRect.height } }, 0.68); } targets.push(...commerceTargets); const sectionCandidates = Array.from(document.querySelectorAll([ "section", "article", "[class*='banner' i]", "[class*='panel' i]", "[class*='visual' i]", "[class*='media' i]", "[class*='feature' i]", "[class*='activity' i]", "[class*='promo' i]", "[class*='recommend' i]", "picture", "video", "canvas" ].join(","))); for (const element of sectionCandidates) { if (targets.filter(target => target.name === "Visual section raster fallback").length >= 10) break; const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); if (!isVisible(element, rect, style)) continue; const absRect = absoluteRect(rect, style); const styles = extractStyles(style); if (!isLikelyFullWidthVisualSection(element, absRect, styles)) continue; const targetRect = clippedViewportRect(absRect, 180); if (!targetRect) continue; addRasterTarget(targets, { name: "Visual section raster fallback", rect: targetRect, mode: "background" }, 0.55); } } const footer = Array.from(document.querySelectorAll("footer, [class*='footer' i]")) .map(element => { const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); if (!isVisible(element, rect, style)) return null; const absRect = absoluteRect(rect, style); if (absRect.y < window.innerHeight * 3) return null; if (absRect.width < window.innerWidth * 0.45 || absRect.height < 180) return null; return { name: "Footer raster fallback", mode: "background", rect: { x: Math.max(0, absRect.x), y: absRect.y, width: Math.min(window.innerWidth, absRect.width), height: Math.min(absRect.height, window.innerHeight * 0.95) } }; }) .filter(Boolean) .sort((a, b) => b.rect.height - a.rect.height)[0]; if (footer) addRasterTarget(targets, footer, 0.75); const complexCards = []; const candidates = Array.from(document.querySelectorAll([ "a", "li", "article", "section", "section div", "[class*='card' i]", "[class*='case' i]", "[class*='customer' i]", "[class*='client' i]", "[class*='solution' i]", "[class*='scene' i]", "[class*='safe' i]", "[class*='security' i]", "[class*='cert' i]", "[class*='swiper-slide' i]" ].join(","))); for (const element of candidates) { if (complexCards.length >= 18) break; const rect = element.getBoundingClientRect(); const style = window.getComputedStyle(element); if (!isVisible(element, rect, style)) continue; const absRect = absoluteRect(rect, style); const styles = extractStyles(style); if (!isProbablyComplexCard(element, absRect, styles)) continue; addRasterTarget(complexCards, { name: "Complex card raster fallback", rect: absRect, mode: "background" }, 0.65); } targets.push(...complexCards); return targets; } async function collectRasterFallbacks(options = {}) { const nodes = []; const assets = {}; const targets = getRasterFallbackTargets(options); const originalX = window.scrollX; const originalY = window.scrollY; const restoreScrollbars = hidePageScrollbars(); try { for (const target of targets) { try { const scrollY = Math.max(0, Math.min(target.rect.y, document.documentElement.scrollHeight - window.innerHeight)); window.scrollTo(0, scrollY); await new Promise(resolve => setTimeout(resolve, 620)); const restorePluginOverlays = hideWebToPPTOverlays(); const restoreText = target.mode === "visual" ? () => {} : hideTextForBackgroundRaster(target.rect); const restoreHeroForeground = target.mode === "hero-background" ? hideHeroForegroundForRaster(target.rect) : () => {}; const restoreOverlays = target.name.includes("Header") ? () => {} : hideFixedTopOverlays(); let asset = null; try { await waitForPaint(target.mode === "visual" ? 20 : 90); asset = await cropViewportRegion(target.rect, window.scrollY); } finally { restoreOverlays(); restoreHeroForeground(); restoreText(); restorePluginOverlays(); } if (!asset?.data) continue; const key = `web-to-ppt-raster://${++rasterCounter}`; assets[key] = { type: "image", mimeType: asset.mimeType || "image/png", data: asset.data }; nodes.push({ id: `raster-${rasterCounter}`, type: "RECTANGLE", tag: "RASTER_FALLBACK", name: target.name, rect: target.rect, visibleRect: target.rect, clipped: false, visualRole: "raster-fallback", section: inferRasterSection(target), ...layerMetaFor("RECTANGLE", { rasterMode: target.mode || "background" }), styles: { display: "block", visibility: "visible", position: target.name.includes("Header") ? "fixed" : "absolute", zIndex: target.name.includes("Header") ? "20" : "1", opacity: "1", overflow: "hidden", backgroundColor: "rgba(0, 0, 0, 0)", backgroundImage: `url("${key}")`, backgroundSize: "100% 100%", backgroundRepeat: "no-repeat", borderTopWidth: "0px", borderRightWidth: "0px", borderBottomWidth: "0px", borderLeftWidth: "0px", boxShadow: "none", filter: "none", color: "rgba(0, 0, 0, 0.85)", transform: "none" }, attributes: { "data-raster-fallback": "true", "data-raster-mode": target.mode || "background", "data-layer-group": "comparison", "data-section": inferRasterSection(target) }, rasterMode: target.mode || "background", backgroundImages: [key], children: [] }); } catch (error) { console.warn("[Web to PPT] Raster fallback failed", error); } } } finally { restoreScrollbars(); window.scrollTo(originalX, originalY); } await new Promise(resolve => setTimeout(resolve, 120)); return { nodes, assets }; } async function collectComparisonFallbacks(options = {}) { const nodes = []; const assets = {}; const documentRect = getDocumentRect(); const width = Math.max(1, Math.round(window.innerWidth || documentRect.width || Number(options.captureWidth || 0) || 1440)); const totalHeight = Math.max(1, Math.round(documentRect.height || document.documentElement.scrollHeight || window.innerHeight)); const sliceHeight = Math.max(320, Math.floor(window.innerHeight * 0.86)); const originalX = window.scrollX; const originalY = window.scrollY; const restoreScrollbars = hidePageScrollbars(); try { let y = 0; let index = 1; while (y < totalHeight - 1) { const height = Math.min(sliceHeight, totalHeight - y); const rect = { x: 0, y, width, height }; try { const scrollY = Math.max(0, Math.min(y, document.documentElement.scrollHeight - window.innerHeight)); window.scrollTo(0, scrollY); await new Promise(resolve => setTimeout(resolve, 420)); const restorePluginOverlays = hideWebToPPTOverlays(); const restoreFixedOverlays = options.keepFixedOverlays === true ? () => {} : hideFixedTopOverlays(); let asset = null; try { await waitForPaint(40); asset = await cropViewportRegion(rect, window.scrollY); } finally { restoreFixedOverlays(); restorePluginOverlays(); } if (asset?.data) { const key = `web-to-ppt-raster://${++rasterCounter}`; assets[key] = { type: "image", mimeType: asset.mimeType || "image/png", data: asset.data }; nodes.push({ id: `raster-${rasterCounter}`, type: "RECTANGLE", tag: "RASTER_FALLBACK", name: `对比底图-${width}px${totalHeight > sliceHeight ? ` ${index}` : ""}`, rect, visibleRect: rect, clipped: false, visualRole: "raster-fallback", section: "document", ...layerMetaFor("RECTANGLE", { rasterMode: "comparison" }), styles: { display: "block", visibility: "visible", position: "absolute", zIndex: "0", opacity: "1", overflow: "hidden", backgroundColor: "rgba(0, 0, 0, 0)", backgroundImage: `url("${key}")`, backgroundSize: "100% 100%", backgroundRepeat: "no-repeat", borderTopWidth: "0px", borderRightWidth: "0px", borderBottomWidth: "0px", borderLeftWidth: "0px", boxShadow: "none", filter: "none", color: "rgba(0, 0, 0, 0.85)", transform: "none" }, attributes: { "data-raster-fallback": "true", "data-raster-mode": "comparison", "data-layer-group": "comparison", "data-section": "document" }, rasterMode: "comparison", backgroundImages: [key], children: [] }); } } catch (error) { console.warn("[Web to PPT] Comparison fallback failed", error); } y += height; index += 1; } } finally { restoreScrollbars(); window.scrollTo(originalX, originalY); } await new Promise(resolve => setTimeout(resolve, 120)); return { nodes, assets }; } function canvasToPng(canvas) { return new Promise(resolve => { canvas.toBlob(blob => { if (!blob) { resolve(null); return; } blobToDataUrl(blob).then(data => resolve({ mimeType: "image/png", data })); }, "image/png"); }); } async function normalizeAssetForPPT(asset) { if (!asset?.data) return asset; if (/^image\/(png|jpe?g|webp)$/i.test(asset.mimeType || "")) { return asset; } try { const image = new Image(); image.decoding = "async"; image.src = asset.data; await new Promise((resolve, reject) => { image.onload = resolve; image.onerror = reject; }); const canvas = document.createElement("canvas"); canvas.width = Math.max(1, image.naturalWidth || image.width || 1); canvas.height = Math.max(1, image.naturalHeight || image.height || 1); const ctx = canvas.getContext("2d"); ctx.drawImage(image, 0, 0, canvas.width, canvas.height); return await canvasToPng(canvas) || asset; } catch (error) { console.warn("[Web to PPT] Image normalization failed", error); return asset; } } async function fetchAsset(url, useProxy) { const videoPoster = await captureVideoPoster(url); if (videoPoster) return videoPoster; if (useProxy) { try { return await proxyFetchAsset(url); } catch (proxyError) { console.warn("[Web to PPT] Proxy image fetch failed, trying direct fetch", proxyError); return directFetchAsset(url); } } try { return await directFetchAsset(url); } catch (directError) { console.warn("[Web to PPT] Direct image fetch failed, trying extension proxy", directError); return proxyFetchAsset(url); } } async function collectAssets(urls, options) { const assets = {}; const queue = Array.from(new Set(Array.from(urls).map(url => normalizeAssetUrl(url)).filter(Boolean))); const rawConcurrency = options.concurrency === "infinite" ? queue.length || 1 : Number(options.concurrency || 8); const concurrency = Math.max(1, Math.min(queue.length || 1, rawConcurrency || 8)); let cursor = 0; async function worker() { while (cursor < queue.length) { const url = queue[cursor++]; try { const result = await normalizeAssetForPPT(await fetchAsset(url, options.useProxy)); assets[url] = { type: "image", mimeType: result.mimeType, data: result.data }; } catch (error) { assets[url] = { type: "image", error: error.message || String(error) }; } } } await Promise.all(Array.from({ length: concurrency }, worker)); return assets; } function collectFonts(root) { const fonts = {}; const walker = document.createTreeWalker(root, NodeFilter.SHOW_ELEMENT); let node = walker.currentNode; while (node) { const style = window.getComputedStyle(node); const family = style.fontFamily; if (family) { fonts[family] = { family, weight: style.fontWeight, style: style.fontStyle }; } node = walker.nextNode(); } return fonts; } function countLayoutNodes(captureNode) { if (!captureNode) return 0; let count = captureNode.layout ? 1 : 0; for (const child of captureNode.children || []) { count += countLayoutNodes(child); } return count; } window.__webToPPTCapture = async function capture(options = {}) { nodeCounter = 0; rasterCounter = 0; const assetUrls = new Set(); const selectionElement = getSelectionElement(options); const selectionRect = getSelectionRect(selectionElement); const diagnostics = { skippedCarouselClones: 0, skippedOutsideClip: 0, selectedElement: Boolean(selectionElement) }; let comparisonFallbacks = { nodes: [], assets: {} }; if (!selectionRect && options.comparisonFallback !== false) { try { comparisonFallbacks = await collectComparisonFallbacks(options); } catch (error) { console.warn("[Web to PPT] Comparison fallback collection skipped", error); } } const documentRect = selectionRect || getDocumentRect(); const root = elementToCaptureNode(selectionElement || document.documentElement, assetUrls, documentRect, diagnostics); const viewportRect = selectionRect || getViewportRect(); let rasterFallbacks = { nodes: [], assets: {} }; if (options.rasterFallbacks !== false && options.captureMode !== "editable") { try { rasterFallbacks = await collectRasterFallbacks(options); } catch (error) { console.warn("[Web to PPT] Raster fallback collection skipped", error); } } if (root?.children) { if (comparisonFallbacks.nodes.length) root.children.push(...comparisonFallbacks.nodes); if (rasterFallbacks.nodes.length) root.children.push(...rasterFallbacks.nodes); } const assets = { ...(await collectAssets(assetUrls, { useProxy: Boolean(options.useProxy), concurrency: options.concurrency || "8" })), ...comparisonFallbacks.assets, ...rasterFallbacks.assets }; return { schema: CAPTURE_SCHEMA, schemaVersion: CAPTURE_SCHEMA_VERSION, format: "web-to-ppt-capture", version: CAPTURE_SCHEMA_VERSION, generatedAt: new Date().toISOString(), source: { title: document.title || location.hostname || "Web Capture", url: location.href, viewport: viewportRect, document: documentRect, devicePixelRatio: window.devicePixelRatio || 1, requestedViewportWidth: Math.max(1, Math.round(Number(options.captureWidth || 0))) || window.innerWidth || viewportRect.width, actualViewportWidth: window.innerWidth, selection: selectionRect ? { x: selectionRect.x, y: selectionRect.y, width: selectionRect.width, height: selectionRect.height, elementName: selectionElement?.tagName?.toLowerCase() || "element" } : null }, canvas: { x: selectionRect?.x || 0, y: selectionRect?.y || 0, width: viewportRect.width, height: documentRect.height, backgroundColor: window.getComputedStyle(document.body).backgroundColor || "#ffffff" }, import: { defaultWidth: selectionRect?.width || Math.max(1, Math.round(Number(options.captureWidth || 0))) || window.innerWidth || viewportRect.width || 1440, captureMode: options.captureMode === "editable" ? "editable" : "mixed" }, nodes: root, assets, fonts: collectFonts(document.documentElement), diagnostics: { nodeCount: nodeCounter, assetCount: Object.keys(assets).length, failedAssetCount: Object.values(assets).filter(asset => asset.error).length, layoutNodeCount: countLayoutNodes(root), comparisonFallbackCount: comparisonFallbacks.nodes.length, rasterFallbackCount: rasterFallbacks.nodes.length, captureMode: options.captureMode === "editable" ? "editable" : "mixed", ...diagnostics } }; }; })();