@@ -102,7 +103,6 @@ const PresentationPage = ({ presentation_id }: { presentation_id: string }) => {
presentationData.slides.length > 0 &&
presentationData.slides.map((slide: any, index: number) => (
-
{renderSlideContent(slide, false)}
))}
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 eb51b36c..704bdf46 100644
--- a/servers/nextjs/app/api/presentation_to_pptx_model/route.ts
+++ b/servers/nextjs/app/api/presentation_to_pptx_model/route.ts
@@ -1,15 +1,13 @@
import { ApiError } from "@/models/errors";
import { NextRequest, NextResponse } from "next/server";
-import puppeteer, { Browser, ElementHandle } from "puppeteer";
+import puppeteer, { Browser, ElementHandle, Page } from "puppeteer";
import { ElementAttributes, SlideAttributesResult } from "@/types/element_attibutes";
import { convertElementAttributesToPptxSlides } from "@/utils/pptx_models_utils";
import { PptxPresentationModel } from "@/types/pptx_models";
import fs from "fs";
import path from "path";
-import crypto from "crypto";
-import sharp from "sharp";
+import { v4 as uuidv4 } from 'uuid';
-// Interface for getAllChildElementsAttributes function arguments
interface GetAllChildElementsAttributesArgs {
element: ElementHandle
;
rootRect?: { left: number; top: number; width: number; height: number } | null;
@@ -23,41 +21,27 @@ interface GetAllChildElementsAttributesArgs {
export async function GET(request: NextRequest) {
let browser: Browser | null = null;
+ let page: Page | null = null;
+
try {
const id = await getPresentationId(request);
- browser = await puppeteer.launch({
- headless: true,
- args: ['--no-sandbox', '--disable-setuid-sandbox']
- });
+ [browser, page] = await getBrowserAndPage(id);
+ const screenshotsDir = getScreenshotsDir();
- // Ensure screenshots directory exists
- const tempDir = process.env.TEMP_DIRECTORY;
- if (!tempDir) {
- console.warn('TEMP_DIRECTORY environment variable not set, skipping screenshot');
- return undefined;
- }
- const screenshotsDir = path.join(tempDir, 'screenshots');
- if (!fs.existsSync(screenshotsDir)) {
- fs.mkdirSync(screenshotsDir, { recursive: true });
- }
-
- const slides = await getSlides(browser, id);
+ const slides = await getSlides(page);
const slides_attributes = await getSlidesAttributes(slides, screenshotsDir);
- const slides_pptx_models = convertElementAttributesToPptxSlides(slides_attributes.elements, slides_attributes.backgroundColors);
+ await postProcessSlidesAttributes(slides_attributes, screenshotsDir);
+ const slides_pptx_models = convertElementAttributesToPptxSlides(slides_attributes);
const presentation_pptx_model: PptxPresentationModel = {
slides: slides_pptx_models,
};
- if (browser) {
- await browser.close();
- }
+ await closeBrowserAndPage(browser, page);
return NextResponse.json(presentation_pptx_model);
} catch (error: any) {
console.error(error);
- if (browser) {
- await browser.close();
- }
+ await closeBrowserAndPage(browser, page);
if (error instanceof ApiError) {
return NextResponse.json(error, { status: 400 });
}
@@ -73,42 +57,19 @@ async function getPresentationId(request: NextRequest) {
return id;
}
-async function getSlidesAttributes(slides: ElementHandle[], screenshotsDir: string) {
- const slideResults: SlideAttributesResult[] = [];
- //? Can't use Promise.all because of the screenshot
- //? taking screenshot with mess up position of elements
- for (const slide of slides) {
- const result = await getAllChildElementsAttributes({ element: slide, screenshotsDir });
- slideResults.push(result);
- }
+async function getBrowserAndPage(id: string): Promise<[Browser, Page]> {
+ const browser = await puppeteer.launch({
+ headless: true,
+ args: [
+ '--no-sandbox',
+ '--disable-setuid-sandbox',
+ '--disable-dev-shm-usage',
+ '--disable-gpu',
+ '--disable-web-security',
+ '--window-size=1920,1080'
+ ],
+ });
- const elements = slideResults.map(result => result.elements);
- const backgroundColors = slideResults.map(result => result.backgroundColor);
-
- return {
- elements,
- backgroundColors
- };
-}
-
-
-async function getSlides(browser: Browser, id: string) {
- const slides_wrapper = await getSlidesWrapper(browser, id);
- const slides = await slides_wrapper.$$(":scope > div > div");
- return slides;
-}
-
-async function getSlidesWrapper(browser: Browser, id: string): Promise> {
- const page = await getPresentationPage(browser, id);
- const slides_wrapper = await page.$("#presentation-slides-wrapper");
- if (!slides_wrapper) {
- throw new ApiError("Presentation slides not found");
- }
- return slides_wrapper;
-}
-
-
-async function getPresentationPage(browser: Browser, id: string) {
const page = await browser.newPage();
page.on('console', (msg) => {
@@ -118,16 +79,82 @@ async function getPresentationPage(browser: Browser, id: string) {
});
await page.setViewport({ width: 1920, height: 1080, deviceScaleFactor: 1 });
- await page.goto(`http://localhost/presentation?id=${id}`, {
+ await page.goto(`http://localhost/pdf-maker?id=${id}`, {
waitUntil: "networkidle0",
timeout: 60000,
});
- return page;
+ return [browser, page];
+}
+
+async function closeBrowserAndPage(browser: Browser | null, page: Page | null) {
+ await page?.close();
+ await browser?.close();
+}
+
+function getScreenshotsDir() {
+ const tempDir = process.env.TEMP_DIRECTORY;
+ if (!tempDir) {
+ console.warn('TEMP_DIRECTORY environment variable not set, skipping screenshot');
+ throw new ApiError('TEMP_DIRECTORY environment variable not set');
+ }
+ const screenshotsDir = path.join(tempDir, 'screenshots');
+ if (!fs.existsSync(screenshotsDir)) {
+ fs.mkdirSync(screenshotsDir, { recursive: true });
+ }
+ return screenshotsDir;
+}
+
+async function postProcessSlidesAttributes(slidesAttributes: SlideAttributesResult[], screenshotsDir: string) {
+ for (const slideAttributes of slidesAttributes) {
+ for (const element of slideAttributes.elements) {
+ if (element.should_screenshot) {
+ const screenshotPath = await screenshotElement(element, screenshotsDir);
+ element.imageSrc = screenshotPath;
+ element.should_screenshot = false;
+ element.element = undefined;
+ }
+ }
+ }
+}
+
+async function screenshotElement(element: ElementAttributes, screenshotsDir: string) {
+ console.log('Taking screenshot of', element.tagName);
+ const screenshotPath = path.join(screenshotsDir, `${uuidv4()}.png`) as `${string}.png`;
+ const screenshot = await element.element?.screenshot({ path: screenshotPath });
+ if (!screenshot) {
+ throw new ApiError("Failed to screenshot element");
+ }
+ return screenshotPath;
}
+async function getSlidesAttributes(slides: ElementHandle[], screenshotsDir: string): Promise {
+ const slideAttributes = await Promise.all(slides.map(async (slide) => {
+ return await getAllChildElementsAttributes({ element: slide, screenshotsDir });
+ }));
+
+ return slideAttributes;
+}
+
+
+async function getSlides(page: Page) {
+ const slides_wrapper = await getSlidesWrapper(page);
+ const slides = await slides_wrapper.$$(":scope > div > div");
+ return slides;
+}
+
+async function getSlidesWrapper(page: Page): Promise> {
+ const slides_wrapper = await page.$("#presentation-slides-wrapper");
+ if (!slides_wrapper) {
+ throw new ApiError("Presentation slides not found");
+ }
+ return slides_wrapper;
+}
+
+
+
+
async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground, inheritedBorderRadius, screenshotsDir }: GetAllChildElementsAttributesArgs): Promise {
- // Get rootRect if not provided (first call)
const currentRootRect = rootRect || await element.evaluate((el) => {
const rect = el.getBoundingClientRect();
return {
@@ -138,67 +165,23 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
};
});
- // Check if this element is SVG or canvas or table
- const tagName = await element.evaluate((el) => el.tagName.toLowerCase());
-
-
- if (tagName === 'svg' || tagName === 'canvas' || tagName === 'table') {
- return {
- elements: [],
- backgroundColor: undefined
- };
-
- // // Get basic attributes for the element
- // const attributes = await getElementAttributes(element);
- // // Take screenshot of SVG/canvas/table element with accurate colors and opacity
- // const screenshotPath = await takeElementScreenshot(element, screenshotsDir);
-
- // // Update image source to point to the screenshot
- // if (screenshotPath) {
- // attributes.imageSrc = screenshotPath;
- // }
-
- // // Adjust position relative to root
- // if (attributes.position && attributes.position.left !== undefined && attributes.position.top !== undefined) {
- // attributes.position = {
- // left: attributes.position.left - currentRootRect.left,
- // top: attributes.position.top - currentRootRect.top,
- // width: attributes.position.width,
- // height: attributes.position.height,
- // };
- // }
-
- // // Return early without processing children for these elements
- // return {
- // elements: [attributes],
- // backgroundColor: undefined
- // };
- }
-
- // Get direct children only (not all descendants)
const directChildElementHandles = await element.$$(':scope > *');
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);
- // 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;
}
- // Apply inherited border radius if element doesn't have it
if (inheritedBorderRadius && !attributes.borderRadius) {
attributes.borderRadius = inheritedBorderRadius;
}
- // Adjust position relative to root
if (attributes.position && attributes.position.left !== undefined && attributes.position.top !== undefined) {
attributes.position = {
left: attributes.position.left - currentRootRect.left,
@@ -208,10 +191,18 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
};
}
- // Add current child to results
+ if (attributes.tagName === 'svg' || attributes.tagName === 'canvas' || attributes.tagName === 'table') {
+ attributes.should_screenshot = true;
+ attributes.element = childElementHandle;
+ }
+
allResults.push({ attributes, depth });
- // Recursively process children of this child
+ //? If the element is a svg, canvas, or table, we don't need to go deeper
+ if (attributes.should_screenshot) {
+ break;
+ }
+
const childResults = await getAllChildElementsAttributes({
element: childElementHandle,
rootRect: currentRootRect,
@@ -224,7 +215,6 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
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;
if (!rootRect) {
const elementsWithRootPosition = allResults.filter(({ attributes }) => {
@@ -243,13 +233,15 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
}
}
- // 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;
const hasImage = attributes.imageSrc;
+ const isSvg = attributes.tagName === 'svg';
+ const isCanvas = attributes.tagName === 'canvas';
+ const isTable = attributes.tagName === 'table';
const isRootPosition = attributes.position &&
attributes.position.left === 0 &&
@@ -257,7 +249,7 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
attributes.position.width === currentRootRect.width &&
attributes.position.height === currentRootRect.height;
- const hasOtherProperties = hasBackground || hasBorder || hasShadow || hasText || hasImage;
+ const hasOtherProperties = hasBackground || hasBorder || hasShadow || hasText || hasImage || isSvg || isCanvas || isTable;
return hasOtherProperties && !isRootPosition;
}) : allResults;
@@ -274,7 +266,6 @@ async function getAllChildElementsAttributes({ element, rootRect = null, 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,
@@ -416,8 +407,6 @@ async function getElementAttributes(element: ElementHandle): Promise): Promise): Promise): Promise): Promise): Promise): Promise 0) {
const shadowColor = colorParts.join(' ');
@@ -509,34 +489,28 @@ async function getElementAttributes(element: ElementHandle): Promise value !== 0);
- // Calculate a score for this shadow (higher is better)
let shadowScore = 0;
if (hasNonZeroValues) {
- // Count non-zero numeric values
shadowScore += numericParts.filter(value => value !== 0).length;
}
if (hasVisibleColor) {
- shadowScore += 2; // Bonus for visible color
+ shadowScore += 2;
}
- // Select this shadow if it has a better score
if ((hasNonZeroValues || hasVisibleColor) && shadowScore > bestShadowScore) {
selectedShadow = shadowStr;
bestShadowScore = shadowScore;
}
}
- // If no meaningful shadow found, use the first one
if (!selectedShadow && shadows.length > 0) {
selectedShadow = shadows[0];
}
if (selectedShadow) {
- // Parse the selected shadow
const shadowParts = selectedShadow.split(' ');
const numericParts: number[] = [];
const colorParts: string[] = [];
@@ -544,7 +518,6 @@ async function getElementAttributes(element: ElementHandle): Promise): Promise): Promise= 2) {
const offsetX = numericParts[0];
const offsetY = numericParts[1];
const blurRadius = numericParts.length >= 3 ? numericParts[2] : 0;
const spreadRadius = numericParts.length >= 4 ? numericParts[3] : 0;
- // Handle color - it can be anywhere in the parts
let shadowColor = 'rgba(0, 0, 0, 0.3)'; // default color
if (colorParts.length > 0) {
shadowColor = colorParts.join(' ');
@@ -602,7 +569,6 @@ async function getElementAttributes(element: ElementHandle): Promise 0 || spreadRadius !== 0 ||
(shadowColorResult.hex && shadowColorResult.hex !== '000000' && shadowColorResult.opacity !== 0);
@@ -650,24 +616,19 @@ async function getElementAttributes(element: ElementHandle): Promise singleLineHeight * 2; // Allow some tolerance
+ const hasTextWrapping = htmlEl.offsetHeight > singleLineHeight * 2;
const hasOverflow = htmlEl.scrollHeight > htmlEl.clientHeight;
const isMultiline = hasExplicitLineBreaks || hasTextWrapping || hasOverflow;
- // Only return line height if text is multiline
if (isMultiline && lineHeight && lineHeight !== 'normal') {
const parsedLineHeight = parseFloat(lineHeight);
if (!isNaN(parsedLineHeight)) {
@@ -804,53 +765,3 @@ async function getElementAttributes(element: ElementHandle): Promise, screenshotsDir: string): Promise {
- try {
- // Check element visibility and dimensions
- const elementInfo = await element.evaluate((el) => {
- const rect = el.getBoundingClientRect();
- const styles = window.getComputedStyle(el);
-
- // Check if element is visible
- const isVisible = styles.visibility !== 'hidden' &&
- styles.display !== 'none' &&
- styles.opacity !== '0';
-
- if (!isVisible || rect.width <= 0 || rect.height <= 0) {
- return null;
- }
-
- return {
- width: rect.width,
- height: rect.height
- };
- });
-
- if (!elementInfo) {
- console.warn('Element is not visible or has invalid dimensions, skipping screenshot');
- return undefined;
- }
-
- // Generate unique filename
- const uuid = crypto.randomUUID();
- const filename = `${uuid}.png`;
- const filePath = path.join(screenshotsDir, filename);
-
- // Take screenshot of the element with accurate colors and opacity
- // This captures the element exactly as rendered in the browser with all CSS styles applied
- await element.screenshot({
- path: filePath as `${string}.png`,
- type: 'png',
- omitBackground: true // Use transparent background for better quality
- });
-
- console.log(`Screenshot saved: ${filePath}`);
- return filePath;
-
- } catch (error) {
- console.error('Error taking element screenshot:', error);
- return undefined;
- }
-}
-
diff --git a/servers/nextjs/types/element_attibutes.ts b/servers/nextjs/types/element_attibutes.ts
index df113fc1..70df71f7 100644
--- a/servers/nextjs/types/element_attibutes.ts
+++ b/servers/nextjs/types/element_attibutes.ts
@@ -1,3 +1,5 @@
+import { ElementHandle } from "puppeteer";
+
export interface ElementAttributes {
tagName: string;
id?: string;
@@ -57,6 +59,8 @@ export interface ElementAttributes {
shape?: 'rectangle' | 'circle';
connectorType?: string;
textWrap?: boolean;
+ should_screenshot?: boolean;
+ element?: ElementHandle;
}
export interface SlideAttributesResult {
diff --git a/servers/nextjs/utils/pptx_models_utils.ts b/servers/nextjs/utils/pptx_models_utils.ts
index 35466f86..ec8d3631 100644
--- a/servers/nextjs/utils/pptx_models_utils.ts
+++ b/servers/nextjs/utils/pptx_models_utils.ts
@@ -1,4 +1,4 @@
-import { ElementAttributes } from "@/types/element_attibutes";
+import { ElementAttributes, SlideAttributesResult } from "@/types/element_attibutes";
import {
PptxSlideModel,
PptxTextBoxModel,
@@ -63,15 +63,14 @@ function convertLineHeightToRelative(lineHeight?: number, fontSize?: number): nu
}
/**
- * Converts ElementAttributes[][] to PptxSlideModel[]
- * Each inner array represents elements on a slide
+ * Converts SlideAttributesResult[] to PptxSlideModel[]
+ * Each SlideAttributesResult represents elements on a slide
*/
export function convertElementAttributesToPptxSlides(
- slidesAttributes: ElementAttributes[][],
- backgroundColors?: (string | undefined)[]
+ slidesAttributes: SlideAttributesResult[]
): PptxSlideModel[] {
- return slidesAttributes.map((slideElements, index) => {
- const shapes = slideElements.map(element => {
+ return slidesAttributes.map((slideAttributes) => {
+ const shapes = slideAttributes.elements.map(element => {
return convertElementToPptxShape(element);
}).filter(Boolean); // Remove any null/undefined shapes
@@ -80,9 +79,9 @@ export function convertElementAttributesToPptxSlides(
};
// Add background color if available
- if (backgroundColors && backgroundColors[index]) {
+ if (slideAttributes.backgroundColor) {
slide.background = {
- color: backgroundColors[index]!,
+ color: slideAttributes.backgroundColor,
opacity: 1.0
};
}