diff --git a/servers/fastapi/models/pptx_models.py b/servers/fastapi/models/pptx_models.py index 12a8aa91..768d31c1 100644 --- a/servers/fastapi/models/pptx_models.py +++ b/servers/fastapi/models/pptx_models.py @@ -54,9 +54,9 @@ class PptxPositionModel(BaseModel): class PptxFontModel(BaseModel): name: str = "Inter" size: int = 16 - bold: bool = False italic: bool = False color: str = "000000" + font_weight: Optional[int] = 400 class PptxFillModel(BaseModel): diff --git a/servers/fastapi/services/pptx_presentation_creator.py b/servers/fastapi/services/pptx_presentation_creator.py index 875951b5..53bd6fb8 100644 --- a/servers/fastapi/services/pptx_presentation_creator.py +++ b/servers/fastapi/services/pptx_presentation_creator.py @@ -57,23 +57,47 @@ class PptxPresentationCreator: async def fetch_network_assets(self): image_urls = [] + models_with_network_asset: List[PptxPictureBoxModel] = [] + + if self._ppt_model.shapes: + for each_shape in self._ppt_model.shapes: + if isinstance(each_shape, PptxPictureBoxModel): + image_path = each_shape.picture.path + if image_path.startswith("http"): + if "app_data/images" in image_path: + relative_path = image_path.split("/app_data/images/")[1] + each_shape.picture.path = os.path.join( + "app_data/images", relative_path + ) + each_shape.picture.is_network = False + continue + image_urls.append(image_path) + models_with_network_asset.append(each_shape) for each_slide in self._slide_models: - models_with_network_asset: List[PptxPictureBoxModel] = [] for each_shape in each_slide.shapes: if isinstance(each_shape, PptxPictureBoxModel): image_path = each_shape.picture.path - if not image_path.startswith("http"): - continue - image_urls.append(image_path) - models_with_network_asset.append(each_shape) + if image_path.startswith("http"): + if "app_data/images" in image_path: + relative_path = image_path.split("/app_data/images/")[1] + each_shape.picture.path = os.path.join( + "app_data/images", relative_path + ) + each_shape.picture.is_network = False + continue + image_urls.append(image_path) + models_with_network_asset.append(each_shape) + if image_urls: image_paths = await download_files(image_urls, self._temp_dir) + for each_shape, each_image_path in zip( models_with_network_asset, image_paths ): - each_shape.picture.path = each_image_path - each_shape.picture.is_network = False + if each_image_path: + each_shape.picture.path = each_image_path + each_shape.picture.is_network = False async def create_ppt(self): await self.fetch_network_assets() @@ -261,6 +285,7 @@ class PptxPresentationCreator: font_json = font.model_dump() font_json["bold"] = True font_json["italic"] = True + font_json["font_weight"] = 700 # Set font weight to bold text_runs.append( PptxTextRunModel( text=text_content, font=PptxFontModel(**font_json) @@ -276,6 +301,7 @@ class PptxPresentationCreator: text_content = line[current_pos + 2 : end_pos] font_json = font.model_dump() font_json["bold"] = True + font_json["font_weight"] = 700 # Set font weight to bold text_runs.append( PptxTextRunModel( text=text_content, font=PptxFontModel(**font_json) @@ -434,9 +460,9 @@ class PptxPresentationCreator: def apply_font(self, font: Font, font_model: PptxFontModel): font.name = font_model.name font.color.rgb = RGBColor.from_string(font_model.color) - font.bold = font_model.bold font.italic = font_model.italic font.size = Pt(font_model.size) + font.bold = font_model.font_weight >= 600 def save(self, path: str): self._ppt.save(path) diff --git a/servers/nextjs/app/api/presentation_to_pptx_model/route.ts b/servers/nextjs/app/api/presentation_to_pptx_model/route.ts index c7f718cb..02da924f 100644 --- a/servers/nextjs/app/api/presentation_to_pptx_model/route.ts +++ b/servers/nextjs/app/api/presentation_to_pptx_model/route.ts @@ -5,6 +5,15 @@ import { ElementAttributes, SlideAttributesResult } from "@/types/element_attibu import { convertElementAttributesToPptxSlides } from "@/utils/pptx_models_utils"; import { PptxPresentationModel } from "@/types/pptx_models"; +// Interface for getAllChildElementsAttributes function arguments +interface GetAllChildElementsAttributesArgs { + element: ElementHandle; + rootRect?: { left: number; top: number; width: number; height: number } | null; + depth?: number; + inheritedFont?: ElementAttributes['font']; + inheritedBackground?: ElementAttributes['background']; +} + export async function GET(request: NextRequest) { let browser: Browser | null = null; @@ -49,7 +58,7 @@ async function getPresentationId(request: NextRequest) { async function getSlidesAttributes(slides: ElementHandle[]) { const slideResults = await Promise.all(slides.map(async (slide) => { - return await getAllChildElementsAttributes(slide); + return await getAllChildElementsAttributes({ element: slide }); })); const elements = slideResults.map(result => result.elements); @@ -84,7 +93,6 @@ async function getPresentationPage(browser: Browser, id: string) { page.on('console', (msg) => { const type = msg.type(); const text = msg.text(); - console.log(`[Puppeteer Console ${type.toUpperCase()}] ${text}`); }); await page.setViewport({ width: 1640, height: 720, deviceScaleFactor: 1 }); @@ -96,8 +104,9 @@ async function getPresentationPage(browser: Browser, id: string) { } -async function getAllChildElementsAttributes(element: ElementHandle): Promise { - const rootRect = await element.evaluate((el) => { +async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground }: GetAllChildElementsAttributesArgs): Promise { + // Get rootRect if not provided (first call) + const currentRootRect = rootRect || await element.evaluate((el) => { const rect = el.getBoundingClientRect(); return { left: isFinite(rect.left) ? rect.left : 0, @@ -107,54 +116,71 @@ async function getAllChildElementsAttributes(element: ElementHandle): P }; }); - const childElementHandles = await element.$$(':scope *'); + // Get direct children only (not all descendants) + const directChildElementHandles = await element.$$(':scope > *'); - const attributesPromises = childElementHandles.map(async (childElementHandle) => { + const allResults: { attributes: ElementAttributes; depth: number }[] = []; + + // Process direct children recursively + for (const childElementHandle of directChildElementHandles) { + // Get attributes for current child const attributes = await getElementAttributes(childElementHandle); - const depth = await childElementHandle.evaluate((el) => { - let depth = 0; - let current = el; - while (current.parentElement) { - depth++; - current = current.parentElement; - } - return depth; - }); + // Apply inherited font only on elements that have direct text + if (inheritedFont && !attributes.font && attributes.innerText && attributes.innerText.trim().length > 0) { + attributes.font = inheritedFont; + } + // Apply inherited background only on elements that have shadow + if (inheritedBackground && !attributes.background && attributes.shadow) { + attributes.background = inheritedBackground; + } + // Adjust position relative to root if (attributes.position && attributes.position.left !== undefined && attributes.position.top !== undefined) { attributes.position = { - left: attributes.position.left - rootRect.left, - top: attributes.position.top - rootRect.top, + left: attributes.position.left - currentRootRect.left, + top: attributes.position.top - currentRootRect.top, width: attributes.position.width, height: attributes.position.height, }; } - return { attributes, depth }; - }); + // Add current child to results + allResults.push({ attributes, depth }); - const allResults = await Promise.all(attributesPromises); + // Recursively process children of this child + const childResults = await getAllChildElementsAttributes({ + element: childElementHandle, + rootRect: currentRootRect, + depth: depth + 1, + inheritedFont: attributes.font || inheritedFont, + inheritedBackground: attributes.background || inheritedBackground, + }); + allResults.push(...childResults.elements.map(attr => ({ attributes: attr, depth: depth + 1 }))); + } + // Find background color from elements with root position (only in first call) let backgroundColor: string | undefined; - const elementsWithRootPosition = allResults.filter(({ attributes }) => { - return attributes.position && - attributes.position.left === 0 && - attributes.position.top === 0 && - attributes.position.width === rootRect.width && - attributes.position.height === rootRect.height; - }); + if (!rootRect) { + const elementsWithRootPosition = allResults.filter(({ attributes }) => { + return attributes.position && + attributes.position.left === 0 && + attributes.position.top === 0 && + attributes.position.width === currentRootRect.width && + attributes.position.height === currentRootRect.height; + }); - for (const { attributes } of elementsWithRootPosition) { - if (attributes.background && attributes.background.color) { - backgroundColor = attributes.background.color; - break; + for (const { attributes } of elementsWithRootPosition) { + if (attributes.background && attributes.background.color) { + backgroundColor = attributes.background.color; + break; + } } } - const filteredResults = allResults.filter(({ attributes }) => { - const hasOwnBackground = attributes.background && attributes.background.color && !attributes.background.isInherited; - const hasInheritedBackground = attributes.background && attributes.background.color && attributes.background.isInherited; + // Filter results (only in first call) + const filteredResults = !rootRect ? allResults.filter(({ attributes }) => { + const hasBackground = attributes.background && attributes.background.color; const hasBorder = attributes.border && attributes.border.color; const hasShadow = attributes.shadow && attributes.shadow.color; const hasText = attributes.innerText && attributes.innerText.trim().length > 0; @@ -163,40 +189,54 @@ async function getAllChildElementsAttributes(element: ElementHandle): P const isRootPosition = attributes.position && attributes.position.left === 0 && attributes.position.top === 0 && - attributes.position.width === rootRect.width && - attributes.position.height === rootRect.height; + attributes.position.width === currentRootRect.width && + attributes.position.height === currentRootRect.height; - // Include elements that have meaningful visual properties - // Elements with own background colors, borders, shadows, text, or images should be included - // Elements with only inherited background colors should only be included if they have other properties - const hasOtherProperties = hasBorder || hasShadow || hasText || hasImage; - const hasVisualProperties = hasOwnBackground || hasOtherProperties || (hasInheritedBackground && hasOtherProperties); + const hasOtherProperties = hasBackground || hasBorder || hasShadow || hasText || hasImage; + return hasOtherProperties && !isRootPosition; + }) : allResults; - return hasVisualProperties && !isRootPosition; - }); + if (!rootRect) { + const sortedElements = filteredResults + .sort((a, b) => { + const zIndexA = a.attributes.zIndex || 0; + const zIndexB = b.attributes.zIndex || 0; - const sortedElements = filteredResults - .sort((a, b) => { - const zIndexA = a.attributes.zIndex || 0; - const zIndexB = b.attributes.zIndex || 0; + if (zIndexA === zIndexB) { + return b.depth - a.depth; + } - if (zIndexA === zIndexB) { - return b.depth - a.depth; - } + return zIndexB - zIndexA; + }) + .map(({ attributes }) => { + // Set background color to backgroundColor for elements that have shadow but no background color + if (attributes.shadow && attributes.shadow.color && (!attributes.background || !attributes.background.color) && backgroundColor) { + attributes.background = { + color: backgroundColor, + opacity: undefined + }; + } + return attributes; + }); - return zIndexB - zIndexA; - }) - .map(({ attributes }) => attributes); - - return { - elements: sortedElements, - backgroundColor - }; + return { + elements: sortedElements, + backgroundColor + }; + } else { + return { + elements: filteredResults.map(({ attributes }) => attributes), + backgroundColor + }; + } } +// Do not edit this function, it is used to get the attributes of an element async function getElementAttributes(element: ElementHandle): Promise { - const attributes = await element.evaluate((el) => { + + const attributes = await element.evaluate((el: Element) => { + function colorToHex(color: string): { hex: string | undefined; opacity: number | undefined } { if (!color || color === 'transparent' || color === 'rgba(0, 0, 0, 0)') { return { hex: undefined, opacity: undefined }; @@ -278,45 +318,12 @@ async function getElementAttributes(element: ElementHandle): Promise): Promise): Promise): Promise 0) { const shadowColor = colorParts.join(' '); const colorResult = colorToHex(shadowColor); hasVisibleColor = !!(colorResult.hex && colorResult.hex !== '000000' && colorResult.opacity !== 0); - console.log(`Shadow ${i} color analysis - color: "${shadowColor}", result: ${JSON.stringify(colorResult)}, hasVisibleColor: ${hasVisibleColor}`); } // Check if we have any non-zero numeric values (offset, blur, or spread) const hasNonZeroValues = numericParts.some(value => value !== 0); - console.log(`Shadow ${i} - hasNonZeroValues: ${hasNonZeroValues}, hasVisibleColor: ${hasVisibleColor}`); - // Calculate a score for this shadow (higher is better) let shadowScore = 0; if (hasNonZeroValues) { @@ -461,18 +462,15 @@ async function getElementAttributes(element: ElementHandle): Promise bestShadowScore) { selectedShadow = shadowStr; bestShadowScore = shadowScore; - console.log(`Selected shadow ${i} with score ${shadowScore}: "${selectedShadow}"`); } } // If no meaningful shadow found, use the first one if (!selectedShadow && shadows.length > 0) { selectedShadow = shadows[0]; - console.log(`No meaningful shadow found, using first: "${selectedShadow}"`); } if (selectedShadow) { - console.log(`Parsing selected shadow: "${selectedShadow}"`); // Parse the selected shadow const shadowParts = selectedShadow.split(' '); @@ -525,8 +523,6 @@ async function getElementAttributes(element: ElementHandle): Promise= 2) { const offsetX = numericParts[0]; @@ -541,14 +537,11 @@ async function getElementAttributes(element: ElementHandle): Promise 0 || spreadRadius !== 0 || (shadowColorResult.hex && shadowColorResult.hex !== '000000' && shadowColorResult.opacity !== 0); - console.log(`Has valid values: ${hasValidValues} (offsetX: ${offsetX}, offsetY: ${offsetY}, blurRadius: ${blurRadius}, spreadRadius: ${spreadRadius}, color: ${shadowColorResult.hex}, opacity: ${shadowColorResult.opacity})`); - if (hasValidValues) { shadow = { offset: [offsetX, offsetY], @@ -559,16 +552,11 @@ async function getElementAttributes(element: ElementHandle): Promise): Promise= 600 : false, + font_weight: element.font.weight ?? 400, italic: element.font.italic ?? false, color: element.font.color ?? "000000" } : undefined; @@ -139,7 +127,7 @@ function convertToTextBox(element: ElementAttributes): PptxTextBoxModel { }; return { - margin, + margin: undefined, fill, position, text_wrap: element.textWrap ?? true, @@ -157,19 +145,7 @@ function convertToAutoShapeBox(element: ElementAttributes): PptxAutoShapeBoxMode width: Math.round(element.position?.width ?? 0), height: Math.round(element.position?.height ?? 0) }; - - const margin: PptxSpacingModel | undefined = element.margin ? { - top: Math.round(element.margin.top ?? 0), - bottom: Math.round(element.margin.bottom ?? 0), - left: Math.round(element.margin.left ?? 0), - right: Math.round(element.margin.right ?? 0) - } : undefined; - - const fill: PptxFillModel | undefined = element.background?.color ? - (element.background.isInherited ? - (element.shadow?.color ? { color: element.background.color } : undefined) : - { color: element.background.color } - ) : undefined; + const fill: PptxFillModel | undefined = element.background?.color ? { color: element.background.color } : undefined; const stroke: PptxStrokeModel | undefined = element.border?.color ? { color: element.border.color, @@ -191,7 +167,7 @@ function convertToAutoShapeBox(element: ElementAttributes): PptxAutoShapeBoxMode font: element.font ? { name: element.font.name ?? "Inter", size: Math.round(element.font.size ?? 16), // int - bold: element.font.weight ? element.font.weight >= 600 : false, + font_weight: element.font.weight ?? 400, italic: element.font.italic ?? false, color: element.font.color ?? "000000" } : undefined, @@ -200,7 +176,7 @@ function convertToAutoShapeBox(element: ElementAttributes): PptxAutoShapeBoxMode return { type: PptxShapeType.ROUNDED_RECTANGLE, // Default to rounded rectangle - margin, + margin: undefined, fill, stroke, shadow, @@ -222,13 +198,6 @@ function convertToPictureBox(element: ElementAttributes): PptxPictureBoxModel { height: Math.round(element.position?.height ?? 0) }; - const margin: PptxSpacingModel | undefined = element.margin ? { - top: Math.round(element.margin.top ?? 0), - bottom: Math.round(element.margin.bottom ?? 0), - left: Math.round(element.margin.left ?? 0), - right: Math.round(element.margin.right ?? 0) - } : undefined; - const objectFit: PptxObjectFitModel = { fit: element.objectFit ? (element.objectFit as PptxObjectFitEnum) : PptxObjectFitEnum.CONTAIN }; @@ -241,7 +210,7 @@ function convertToPictureBox(element: ElementAttributes): PptxPictureBoxModel { return { position, - margin, + margin: undefined, clip: element.clip ?? true, overlay: element.overlay, border_radius: element.borderRadius ? element.borderRadius.map(r => Math.round(r)) : undefined, // List[int] - 4 elements from route parsing