/** * 图片提取 - 主图、SKU、详情图、视频 * 从 extension-v1 移植,新增: * - srcset 处理(Ozon 画廊是 / ) * - toOriginalUrl 传平台规则(Ozon /wc\d+/) */ import { toAbsoluteUrl, toOriginalUrl, urlInBrackets, looksLikeImageUrl, dedupeKey, pickBestFromSrcset, } from './url'; import { queryAllDeep } from './dom'; import type { ImageGroupKey, SiteProfile, SrcProp } from '../profiles/types'; export interface ImageMaterial { key: string; // 'main-001' groupKey: ImageGroupKey; // 'main' groupName: string; // '主图' variantName?: string; // SKU 规格名(仅 sku 组) url: string; // 已还原为原图 thumbUrl: string; // 页面上的原始小图地址 index: number; type: 'img' | 'video'; width?: number; height?: number; } /** 从元素上读出图片地址与名称,按 srcProps 顺序降级 */ function readImageSource( el: Element, srcProps: SrcProp[], nameSelectors?: string[] ): { url: string; name: string; imgEl: HTMLImageElement | null } { let url = ''; let name = ''; let imgEl: HTMLImageElement | null = el instanceof HTMLImageElement ? el : null; for (const prop of srcProps) { if (url) break; if (prop === 'backgroundImage') { if (el.tagName === 'IMG') { const img = el as HTMLImageElement; url = img.currentSrc || img.src || ''; name = img.alt || ''; } else { const bg = getComputedStyle(el).backgroundImage || ''; const cand = (urlInBrackets(bg) || bg).replace(/['"]/g, ''); if (looksLikeImageUrl(cand)) url = cand; } continue; } if (prop === 'srcset') { // const raw = el.getAttribute('srcset') || (el as any).srcset || ''; if (raw) url = pickBestFromSrcset(raw); continue; } const raw = (el as any)[prop] || el.getAttribute(prop); if (raw) { // srcset 场景下 currentSrc 才是实际加载的那张 url = prop === 'src' ? ((el as HTMLImageElement).currentSrc || (el as HTMLImageElement).src || '') : raw; } } // 选择器命中的是容器、图在子节点上 if (!url && el.tagName !== 'IMG') { const inner = el.querySelector('img, source'); if (inner) { const srcset = inner.getAttribute('srcset'); url = srcset ? pickBestFromSrcset(srcset) : inner.getAttribute('data-src') || (inner as HTMLImageElement).currentSrc || (inner as HTMLImageElement).src || ''; if (inner instanceof HTMLImageElement) imgEl = inner; if (!name && inner instanceof HTMLImageElement) name = inner.alt || ''; } } // 名称统一取(SKU 规格名) if (!name && nameSelectors?.length) { for (const sel of nameSelectors) { const t = el.querySelector(sel)?.textContent?.trim(); if (t) { name = t; break; } } } return { url: url ? toAbsoluteUrl(url) : '', name, imgEl }; } export function collectImages(profile: SiteProfile): ImageMaterial[] { const result: ImageMaterial[] = []; for (const group of profile.imageGroups) { const srcProps = group.srcProps ?? profile.defaultSrcProps; // 去重按组独立:一张图同时是主图和 SKU 图是正常的 const seen = new Set(); const activeSet = new Set(group.activeSelectors ? queryAllDeep(group.activeSelectors) : []); for (const el of queryAllDeep(group.selectors)) { if (activeSet.has(el)) continue; if (group.excludeWithin?.some((sel) => el.closest(sel))) continue; const { url: rawUrl, name, imgEl } = readImageSource(el, srcProps, group.nameSelectors); if (!rawUrl) continue; if (group.type === 'video' && !/\.(mp4|avi|mov|wmv|m3u8|webm)(\?|$)/i.test(rawUrl) && !/^blob:/i.test(rawUrl)) { continue; } const url = group.type === 'img' ? toOriginalUrl(rawUrl, profile.originalUrlRules) : rawUrl; // 尺寸过滤 if (group.type === 'img' && (group.minWidth || group.minHeight)) { const measured = imgEl ?? (el as HTMLElement); const w = (measured as HTMLImageElement).naturalWidth || (measured as HTMLElement).offsetWidth || 0; const h = (measured as HTMLImageElement).naturalHeight || (measured as HTMLElement).offsetHeight || 0; if (w > 0 && h > 0 && (w < (group.minWidth ?? 0) || h < (group.minHeight ?? 0))) continue; } const k = group.key === 'sku' ? `${dedupeKey(url, profile.originalUrlRules)}::${name}` : dedupeKey(url, profile.originalUrlRules); if (seen.has(k)) continue; seen.add(k); result.push({ key: `${group.key}-${String(result.filter((r) => r.groupKey === group.key).length + 1).padStart(3, '0')}`, groupKey: group.key, groupName: group.name, variantName: group.key === 'sku' ? name || undefined : undefined, url, thumbUrl: rawUrl, index: result.length, type: group.type, }); } } return result; }