← Files DefuddleARCHIVED FILE

skills/defuddle-web-content/references/source/src/removals/small-images.ts

6.05 KB · Oct 5, 2026 · 18:35 UTC

↓ Download file

import { logDebug } from '../utils';
import { getClassName } from '../utils/dom';
import { isBase64Placeholder } from '../elements/images';
import { LOOKS_LIKE_LATEX_RE } from '../elements/math.base';

const STYLE_WIDTH_PATTERN = /width\s*:\s*(\d+)/;
const STYLE_HEIGHT_PATTERN = /height\s*:\s*(\d+)/;
// Matches width hints in CDN image URLs: ?w=48, &width=48, /w:48/, /width:48/
const URL_WIDTH_PATTERN = /(?:width[=:/]|[/,?&]w[_:=])(\d+)/;

export function getElementIdentifier(element: Element): string | null {
	// Try to create a unique identifier using various attributes
	if (element.tagName.toLowerCase() === 'img') {
		// For lazy-loaded images, use data-src as identifier if available
		const dataSrc = element.getAttribute('data-src');
		if (dataSrc) return `src:${dataSrc}`;

		const src = element.getAttribute('src') || '';
		const srcset = element.getAttribute('srcset') || '';
		const dataSrcset = element.getAttribute('data-srcset');

		if (src) return `src:${src}`;
		if (srcset) return `srcset:${srcset}`;
		if (dataSrcset) return `srcset:${dataSrcset}`;
	}

	const id = element.id || '';
	const className = getClassName(element);
	const viewBox = element.tagName.toLowerCase() === 'svg' ? element.getAttribute('viewBox') || '' : '';

	if (id) return `id:${id}`;
	if (viewBox) return `viewBox:${viewBox}`;
	if (className) return `class:${className}`;

	return null;
}

// Find small IMG and SVG elements
export function findSmallImages(doc: Document, debug: boolean): Set<string> {
	const MIN_DIMENSION = 33;
	const smallImages = new Set<string>();
	let processedCount = 0;

	const elements = doc.querySelectorAll('img, svg');
	const defaultView = doc.defaultView;
	const isBrowser = typeof window !== 'undefined' && defaultView === window;

	for (const element of elements) {
		const attrWidth = parseInt(element.getAttribute('width') || '0');
		const attrHeight = parseInt(element.getAttribute('height') || '0');

		// For SVGs, use viewBox dimensions as fallback when no explicit width/height
		let viewBoxWidth = 0, viewBoxHeight = 0;
		if (element.tagName.toLowerCase() === 'svg') {
			const viewBox = element.getAttribute('viewBox');
			if (viewBox) {
				const parts = viewBox.split(/[\s,]+/);
				if (parts.length === 4) {
					viewBoxWidth = parseFloat(parts[2]) || 0;
					viewBoxHeight = parseFloat(parts[3]) || 0;
				}
			}
		}

		// Check inline style dimensions
		const style = element.getAttribute('style') || '';
		const styleWidth = parseInt(style.match(STYLE_WIDTH_PATTERN)?.[1] || '0');
		const styleHeight = parseInt(style.match(STYLE_HEIGHT_PATTERN)?.[1] || '0');

		// Use getComputedStyle and getBoundingClientRect only in browser
		let computedWidth = 0, computedHeight = 0;
		if (isBrowser) {
			try {
				const cs = defaultView!.getComputedStyle(element);
				computedWidth = parseInt(cs.width) || 0;
				computedHeight = parseInt(cs.height) || 0;
			} catch (e) {}
			try {
				const rect = element.getBoundingClientRect();
				if (rect.width > 0) computedWidth = computedWidth || rect.width;
				if (rect.height > 0) computedHeight = computedHeight || rect.height;
			} catch (e) {}
		}

		const widths = [attrWidth, styleWidth, computedWidth, viewBoxWidth].filter(d => d > 0);
		const heights = [attrHeight, styleHeight, computedHeight, viewBoxHeight].filter(d => d > 0);

		// Fallback: when no explicit dimensions, check srcset 1x URL width hint.
		// Images served at ≤32px via CDN params (e.g. ?w=32 1x) are icons.
		if (widths.length === 0 && heights.length === 0 && element.tagName.toLowerCase() === 'img') {
			const srcset = element.getAttribute('srcset') || '';
			// Find the 1x descriptor entry (or the smallest width descriptor)
			const oneXMatch = srcset.match(/(\S+)\s+1x/);
			if (oneXMatch) {
				const urlWidth = parseInt(oneXMatch[1].match(URL_WIDTH_PATTERN)?.[1] || '0');
				if (urlWidth > 0) widths.push(urlWidth);
			}
		}

		if (widths.length > 0 || heights.length > 0) {
			const effectiveWidth = widths.length > 0 ? Math.min(...widths) : Infinity;
			const effectiveHeight = heights.length > 0 ? Math.min(...heights) : Infinity;

			if (effectiveWidth < MIN_DIMENSION || effectiveHeight < MIN_DIMENSION) {
				// Skip math/equation images
				if (element.tagName.toLowerCase() === 'img') {
					const alt = element.getAttribute('alt') || '';
					if (LOOKS_LIKE_LATEX_RE.test(alt)) continue;
					if (element.classList.contains('latex') || element.classList.contains('tex')) continue;
					if (element.getAttribute('data-latex') || element.getAttribute('data-math')) continue;
				}

				const identifier = getElementIdentifier(element);
				if (identifier) {
					smallImages.add(identifier);
					processedCount++;
				}
			}
		}
	}

	logDebug(debug, 'Found small elements:', processedCount);
	return smallImages;
}

export function removeSmallImages(doc: Document, smallImages: Set<string>, debug: boolean) {
	let removedCount = 0;

	['img', 'svg'].forEach(tag => {
		const elements = doc.getElementsByTagName(tag);
		Array.from(elements).forEach(element => {
			// Remove images with no source information at all (broken/empty images)
			// or unresolvable base64 placeholder images (e.g. lazy-loaded images
			// where JS never ran to inject the real URL)
			if (tag === 'img') {
				const src = element.getAttribute('src') || '';
				const hasAltSrc =
					element.getAttribute('srcset') ||
					element.getAttribute('data-src') ||
					element.getAttribute('data-srcset') ||
					element.getAttribute('data-lazy-src') ||
					element.getAttribute('data-original');
				if (!src && !hasAltSrc) {
					element.remove();
					removedCount++;
					return;
				}
				// Base64 placeholder with no alternative source — unresolvable.
				// Skip images inside <picture>; the picture transform will
				// resolve them from <source> srcsets.
				if (!hasAltSrc && !element.closest('picture') && isBase64Placeholder(src)) {
					element.remove();
					removedCount++;
					return;
				}
			}
			const identifier = getElementIdentifier(element);
			if (identifier && smallImages.has(identifier)) {
				element.remove();
				removedCount++;
			}
		});
	});

	logDebug(debug, 'Removed small elements:', removedCount);
}

SHA-256: eb5de1e4ecbca73e665ac728c702bfe2f1ce6b660c39d801e49d397308fdb3f7