/** * Picture node parser — handles images, video placeholders, and audio placeholders. */ import type { SafeXmlNode } from '../../parser/xml-parser' import { type BaseNodeData, parseBaseProps } from './base-node' interface CropRect { top: number bottom: number left: number right: number } export interface PicNodeData extends BaseNodeData { nodeType: 'picture' blipEmbed?: string blipLink?: string crop?: CropRect /** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */ fill?: SafeXmlNode /** @internal Raw XML node — opaque to consumers. Use serializePresentation() for JSON-safe data. */ line?: SafeXmlNode isVideo?: boolean isAudio?: boolean mediaRId?: string } /** OOXML encodes srcRect percentages as 1/100000 of full extent. */ const CROP_DIVISOR = 100000 /** * Parse a picture XML node (`p:pic`) into PicNodeData. */ export function parsePicNode(picNode: SafeXmlNode): PicNodeData { const base = parseBaseProps(picNode) // --- Blip fill --- const blipFill = picNode.child('blipFill') const blip = blipFill.child('blip') // Try both namespaced and non-namespaced embed attribute const blipEmbed = blip.attr('embed') ?? blip.attr('r:embed') const blipLink = blip.attr('link') ?? blip.attr('r:link') // --- Crop (srcRect) --- const srcRect = blipFill.child('srcRect') let crop: CropRect | undefined if (srcRect.exists()) { const t = srcRect.numAttr('t') const b = srcRect.numAttr('b') const l = srcRect.numAttr('l') const r = srcRect.numAttr('r') if (t !== undefined || b !== undefined || l !== undefined || r !== undefined) { crop = { top: (t ?? 0) / CROP_DIVISOR, bottom: (b ?? 0) / CROP_DIVISOR, left: (l ?? 0) / CROP_DIVISOR, right: (r ?? 0) / CROP_DIVISOR, } } } // --- Shape properties (fill + line) --- const spPr = picNode.child('spPr') const solidFill = spPr.child('solidFill') const gradFill = spPr.child('gradFill') const fill = solidFill.exists() ? solidFill : gradFill.exists() ? gradFill : undefined const ln = spPr.child('ln') const line = ln.exists() ? ln : undefined // --- Video / Audio detection --- const nvPicPr = picNode.child('nvPicPr') const nvPr = nvPicPr.child('nvPr') const videoFile = nvPr.child('videoFile') const audioFile = nvPr.child('audioFile') const isVideo = videoFile.exists() const isAudio = audioFile.exists() let mediaRId: string | undefined if (isVideo) { mediaRId = videoFile.attr('link') ?? videoFile.attr('r:link') } else if (isAudio) { mediaRId = audioFile.attr('link') ?? audioFile.attr('r:link') } return { ...base, nodeType: 'picture', blipEmbed, blipLink, crop, fill, line, isVideo: isVideo || undefined, isAudio: isAudio || undefined, mediaRId, } }