diff --git a/app/ipc/slide_metadata.ts b/app/ipc/slide_metadata.ts index ddd94c40..d6771268 100644 --- a/app/ipc/slide_metadata.ts +++ b/app/ipc/slide_metadata.ts @@ -1,8 +1,7 @@ -import { ipcMain } from "electron"; -import puppeteer from "puppeteer"; +import { BrowserWindow, ipcMain } from "electron"; import fs from 'fs'; import path from 'path'; -import { baseDir, isDev, tempDir } from "../utils/constants"; +import { tempDir } from "../utils/constants"; interface Position { left: number; top: number; @@ -80,255 +79,271 @@ interface GraphElement { type SlideElement = TextElement | PictureElement | BoxElement | LineElement | GraphElement; -interface SlideMetadata { - slideIndex: number; - backgroundColor: string; - elements: SlideElement[]; -} export function setupSlideMetadataHandlers() { ipcMain.handle("get-slide-metadata", async (_, url: string, theme: string, customColors?: any) => { - console.log("get-slide-metadata", process.env.NEXT_PUBLIC_FAST_API, url); - let browser; + let win:BrowserWindow | null = null; + try { - browser = await puppeteer.launch({ - headless: true, - executablePath: isDev ? undefined : path.join(baseDir, "dependencies/chrome-headless-shell/linux_build/chrome-headless-shell"), - args: ['--no-sandbox', '--disable-setuid-sandbox'] - }); + win = new BrowserWindow({ + width: 1920, + height: 1080, + webPreferences: { + webSecurity: false, + preload: path.join(__dirname, '../preloads/index.js'), + }, + show: false, + }); - const page = await browser.newPage(); - await page.setViewport({ width: 1440, height: 900, deviceScaleFactor: 1 }); - // Inject the environment variables before loading the page - await page.evaluateOnNewDocument(` - window.env = { - NEXT_PUBLIC_FAST_API: "${process.env.NEXT_PUBLIC_FAST_API}", - }; - `); - - await page.goto(url, { waitUntil: "networkidle0", timeout: 60000 }); - - await page.waitForSelector('[data-element-type="slide-container"]', { timeout: 80000 }); - - // Apply theme - await page.evaluate((params: any) => { - const { theme, customColors } = params; - document.querySelectorAll(".slide-theme").forEach((container) => { - container.setAttribute("data-theme", theme); + await win.loadURL(url,{userAgent:'electron'}); + + + await win.webContents.executeJavaScript(` + new Promise((resolve) => { + const check = () => { + const el = document.querySelector('[data-element-type="slide-container"]'); + if (el) return resolve(true); + setTimeout(check, 200); + }; + check(); }); + `); + const metadata = await win.webContents.executeJavaScript( +` + (() => { + const rgbToHex = (color) => { + if (!color || color === "transparent" || color === "none") return "000000"; + if (color.startsWith("#")) return color.replace("#", ""); + const matches = color.match(/\\d+/g); + if (!matches) return "000000"; + const [r, g, b] = matches.map(x => parseInt(x)); + return [r, g, b].map(x => x.toString(16).padStart(2, "0")).join(""); + }; - if (theme === "custom" && customColors) { - const root = document.documentElement; - root.style.setProperty("--custom-slide-bg", customColors.slideBg); - root.style.setProperty("--custom-slide-title", customColors.slideTitle); - root.style.setProperty("--custom-slide-heading", customColors.slideHeading); - root.style.setProperty("--custom-slide-description", customColors.slideDescription); - root.style.setProperty("--custom-slide-box", customColors.slideBox); - } - }, { theme, customColors }); + const slidesMetadata = []; + const slideContainers = document.querySelectorAll('[data-element-type="slide-container"]'); - // Get slide metadata - const metadata = await page.evaluate(() => { - const rgbToHex = (color: string): string => { - if (!color || color === "transparent" || color === "none") return "000000"; - if (color.startsWith("#")) return color.replace("#", ""); - const matches = color.match(/\d+/g); - if (!matches) return "000000"; - const [r, g, b] = matches.map(x => parseInt(x)); - return [r, g, b].map(x => x.toString(16).padStart(2, "0")).join(""); - }; + slideContainers.forEach((container) => { + const containerEl = container; + containerEl.style.width = "1280px"; + containerEl.style.height = "720px"; + containerEl.style.transform = "none"; - const slidesMetadata: SlideMetadata[] = []; - const slideContainers = document.querySelectorAll('[data-element-type="slide-container"]'); + const containerRect = containerEl.getBoundingClientRect(); + const slideIndex = parseInt(containerEl.getAttribute("data-slide-index") || "0"); + const backgroundColor = rgbToHex(window.getComputedStyle(containerEl).backgroundColor); - slideContainers.forEach((container) => { - const containerEl = container as HTMLElement; - containerEl.style.width = "1280px"; - containerEl.style.height = "720px"; - containerEl.style.transform = "none"; + const elements = []; + const slideElements = containerEl.querySelectorAll('[data-slide-element]:not([data-element-type="slide-container"])'); - const containerRect = containerEl.getBoundingClientRect(); - const slideIndex = parseInt(containerEl.getAttribute("data-slide-index") || "0"); - const backgroundColor = rgbToHex(window.getComputedStyle(containerEl).backgroundColor); + slideElements.forEach((element) => { + const el = element; + const elementRect = el.getBoundingClientRect(); + const computedStyle = window.getComputedStyle(el); - const elements: SlideElement[] = []; - const slideElements = containerEl.querySelectorAll('[data-slide-element]:not([data-element-type="slide-container"])'); + const position = { + left: Math.round(elementRect.left - containerRect.left), + top: Math.round(elementRect.top - containerRect.top), + width: Math.round(elementRect.width), + height: Math.round(elementRect.height), + }; - slideElements.forEach((element) => { - const el = element as HTMLElement; - const elementRect = el.getBoundingClientRect(); - const computedStyle = window.getComputedStyle(el); + const elementType = el.getAttribute("data-element-type"); + if (!elementType) return; - const position: Position = { - left: Math.round(elementRect.left - containerRect.left), - top: Math.round(elementRect.top - containerRect.top), - width: Math.round(elementRect.width), - height: Math.round(elementRect.height), - }; + switch (elementType) { + case "text": + elements.push({ + position, + paragraphs: [{ + alignment: el.getAttribute("data-is-align") === 'true' ? 2 : 1, + text: el.getAttribute("data-text-content") || el.textContent || "", + font: { + name: computedStyle.fontFamily.split('_')[2] || 'Inter', + size: parseInt(computedStyle.fontSize), + bold: parseInt(computedStyle.fontWeight) >= 500, + weight: parseInt(computedStyle.fontWeight), + color: rgbToHex(computedStyle.color), + }, + }], + }); + break; - const elementType = el.getAttribute("data-element-type"); - if (!elementType) return; + case "picture": + const imgEl = el.tagName.toLowerCase() === "img" ? el : el.querySelector("img"); + if (imgEl) { + elements.push({ + position, + picture: { + is_network: imgEl.src.startsWith("http"), + path: imgEl.src || imgEl.getAttribute("data-image-path") || "", + }, + shape: imgEl.getAttribute('data-image-type'), + object_fit: { + fit: imgEl.getAttribute('data-object-fit'), + focus: [ + parseFloat(imgEl.getAttribute('data-focial-point-x') || '0'), + parseFloat(imgEl.getAttribute('data-focial-point-y') || '0'), + ], + }, + overlay: el.getAttribute("data-is-icon") ? "ffffff" : null, + border_radius: Array(4).fill(parseInt(computedStyle.borderRadius) || 0), + }); + } + break; - switch (elementType) { - case "text": - elements.push({ - position, - paragraphs: [{ - alignment: el.getAttribute("data-is-align") === 'true' ? 2 : 1, - text: el.getAttribute("data-text-content") || el.textContent || "", - font: { - name: computedStyle.fontFamily.split('_')[2] || 'Inter', - size: parseInt(computedStyle.fontSize), - bold: parseInt(computedStyle.fontWeight) >= 500, - weight: parseInt(computedStyle.fontWeight), - color: rgbToHex(computedStyle.color), - }, - }], - } as TextElement); - break; - - case "picture": - const imgEl = el.tagName.toLowerCase() === "img" ? el as HTMLImageElement : el.querySelector("img") as HTMLImageElement; - if (imgEl) { + case "graph": elements.push({ position, picture: { - is_network: imgEl.src.startsWith("http"), - path: imgEl.src || imgEl.getAttribute("data-image-path") || "", + is_network: true, + path: \`__GRAPH_PLACEHOLDER__\${el.getAttribute("data-element-id")}\`, }, - shape: imgEl.getAttribute('data-image-type'), - object_fit: { - fit: imgEl.getAttribute('data-object-fit'), - focus: [ - parseFloat(imgEl.getAttribute('data-focial-point-x') || '0'), - parseFloat(imgEl.getAttribute('data-focial-point-y') || '0'), - ], - }, - overlay: el.getAttribute("data-is-icon") ? "ffffff" : null, - border_radius: Array(4).fill(parseInt(computedStyle.borderRadius) || 0), - } as PictureElement); - } - break; + border_radius: [0, 0, 0, 0], + }); + break; - case "graph": - elements.push({ - position, - picture: { - is_network: true, - path: `__GRAPH_PLACEHOLDER__${el.getAttribute("data-element-id")}`, - }, - border_radius: [0, 0, 0, 0], - } as GraphElement); - break; + case "slide-box": + case "filledbox": + const boxShadow = computedStyle.boxShadow; + let shadowRadius = 0; + let shadowColor = "000000"; + let shadowOffsetX = 0; + let shadowOffsetY = 0; + let shadowOpacity = 0; - case "slide-box": - case "filledbox": - const boxShadow = computedStyle.boxShadow; - let shadowRadius = 0; - let shadowColor = "000000"; - let shadowOffsetX = 0; - let shadowOffsetY = 0; - let shadowOpacity = 0; + if (boxShadow && boxShadow !== "none") { + const boxShadowRegex = + /rgba?\\((\\d+),\\s*(\\d+),\\s*(\\d+),?\\s*([\\d.]+)?\\)?\\s+(-?\\d+)px\\s+(-?\\d+)px\\s+(-?\\d+)px/; + const match = boxShadow.match(boxShadowRegex); - if (boxShadow && boxShadow !== "none") { - const boxShadowRegex = - /rgba?\((\d+),\s*(\d+),\s*(\d+),?\s*([\d.]+)?\)?\s+(-?\d+)px\s+(-?\d+)px\s+(-?\d+)px/; - const match = boxShadow.match(boxShadowRegex); - - if (match) { - const r = match[1]; - const g = match[2]; - const b = match[3]; - const rgbStr = "rgb(" + r + ", " + g + ", " + b + ")"; - shadowColor = rgbToHex(rgbStr); - shadowOpacity = match[4] ? parseFloat(match[4]) : 1; - shadowOffsetX = parseInt(match[5]); - shadowOffsetY = parseInt(match[6]); - shadowRadius = parseInt(match[7]); + if (match) { + const r = match[1]; + const g = match[2]; + const b = match[3]; + const rgbStr = "rgb(" + r + ", " + g + ", " + b + ")"; + shadowColor = rgbToHex(rgbStr); + shadowOpacity = match[4] ? parseFloat(match[4]) : 1; + shadowOffsetX = parseInt(match[5]); + shadowOffsetY = parseInt(match[6]); + shadowRadius = parseInt(match[7]); + } } - } - elements.push({ - position, - type: - computedStyle.borderRadius === "9999px" || - computedStyle.borderRadius === "50%" - ? 9 - : 5, - fill: { - color: rgbToHex(computedStyle.backgroundColor), - }, - border_radius: parseInt(computedStyle.borderRadius) || 0, - stroke: { - color: rgbToHex(computedStyle.borderColor), - thickness: parseInt(computedStyle.borderWidth) || 0, - }, - shadow: { - radius: shadowRadius, - color: shadowColor, - offset: Math.sqrt( - shadowOffsetX * shadowOffsetX + - shadowOffsetY * shadowOffsetY - ), - opacity: shadowOpacity, - angle: Math.round( - (Math.atan2(shadowOffsetY, shadowOffsetX) * 180) / Math.PI - ), - }, - }); - break; + elements.push({ + position, + type: + computedStyle.borderRadius === "9999px" || + computedStyle.borderRadius === "50%" + ? 9 + : 5, + fill: { + color: rgbToHex(computedStyle.backgroundColor), + }, + border_radius: parseInt(computedStyle.borderRadius) || 0, + stroke: { + color: rgbToHex(computedStyle.borderColor), + thickness: parseInt(computedStyle.borderWidth) || 0, + }, + shadow: { + radius: shadowRadius, + color: shadowColor, + offset: Math.sqrt( + shadowOffsetX * shadowOffsetX + + shadowOffsetY * shadowOffsetY + ), + opacity: shadowOpacity, + angle: Math.round( + (Math.atan2(shadowOffsetY, shadowOffsetX) * 180) / Math.PI + ), + }, + }); + break; + case "line": - elements.push({ - position, - lineType: 1, - thickness: computedStyle.borderWidth || computedStyle.height, - color: rgbToHex( - computedStyle.borderColor || computedStyle.backgroundColor - ), - }); - break; - } + elements.push({ + position, + lineType: 1, + thickness: computedStyle.borderWidth || computedStyle.height, + color: rgbToHex( + computedStyle.borderColor || computedStyle.backgroundColor + ), + }); + break; + } + }); + + slidesMetadata.push({ slideIndex, backgroundColor, elements }); }); - slidesMetadata.push({ slideIndex, backgroundColor, elements }); - }); + return slidesMetadata; + })(); +` + ) + // ✅ Handle Graphs: capture each graph element as an image + const graphIds: { id: string; bounds: Electron.Rectangle }[] = await win.webContents.executeJavaScript(` + (() => { + return Array.from(document.querySelectorAll('[data-element-type="graph"]')).map(el => el.getAttribute("data-element-id")); + })(); + `); + + for (const id of graphIds) { + try { + // Scroll into view first + await win.webContents.executeJavaScript(` + document.querySelector('[data-element-id="${id}"]').scrollIntoView({ behavior: 'instant', block: 'center' }); + + `); + // Wait a bit for any animations/rendering to complete + await new Promise((r) => setTimeout(r, 2000)); + + const bounds: Electron.Rectangle = await win.webContents.executeJavaScript(` + (() => { + const el = document.querySelector('[data-element-id="${id}"]'); + if (!el) return null; + const rect = el.getBoundingClientRect(); + return { + x: Math.round(rect.left), + y: Math.round(rect.top), + width: Math.round(rect.width), + height: Math.round(rect.height), + }; + })(); + `); - return slidesMetadata; - }); - - // Handle graph elements - const graphElements = await page.$$('[data-element-type="graph"]'); - for (const graphElement of graphElements) { - const graphId = await graphElement.evaluate(el => el.getAttribute("data-element-id")); - const screenshot = await graphElement.screenshot({ - type: "jpeg", - encoding: "base64", - quality: 100, - omitBackground: true, - }); - - - const filename = `chart-${graphId}-${Date.now()}.jpg`; - const filePath = path.join(tempDir, filename); - fs.writeFileSync(filePath, Buffer.from(screenshot, 'base64')); - - metadata.forEach(slide => { - slide.elements.forEach(element => { - if ('picture' in element && element.picture.path === `__GRAPH_PLACEHOLDER__${graphId}`) { + const image = await win.webContents.capturePage(bounds); + const buffer = image.toJPEG(100); + + + if (buffer.length === 0) { + console.error("Empty buffer! Graph not captured."); + continue; + } + + const filePath = path.join(tempDir, `chart-${id}-${Date.now()}.jpeg`); + fs.writeFileSync(filePath, buffer); + + // Update metadata + metadata.forEach((slide: any) => { + slide.elements.forEach((element: any) => { + if ("picture" in element && element.picture.path === `__GRAPH_PLACEHOLDER__${id}`) { element.picture.path = filePath; } }); }); + } catch (err) { + console.error(`Failed to capture or save chart-${id}:`, err); } - + } return metadata; } catch (error) { console.error("Error during page preparation:", error); throw error; } finally { - if (browser) await browser.close(); + // if (browser) await browser.close(); + if (win) win.close(); } }); } diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/AllChart.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/AllChart.tsx index efa53c75..687b6306 100644 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/AllChart.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/AllChart.tsx @@ -30,24 +30,25 @@ const AllChart = ({ const slide = state.presentationGeneration?.presentationData?.slides[slideIndex]; - const style = slide?.content.graph.style; + + const style = slide?.content.graph.style || {}; return Object.keys( style === null || style === undefined ? {} : (style as ChartSettings) ).length > 0 ? (style as ChartSettings) : { - showLegend: false, - showGrid: false, - showAxisLabel: true, - showDataLabel: true, - dataLabel: { - dataLabelPosition: - slide?.content.graph.type === "pie" - ? ("Outside" as const) - : ("Inside" as const), - dataLabelAlignment: "Center" as const, - }, - }; + showLegend: false, + showGrid: false, + showAxisLabel: true, + showDataLabel: true, + dataLabel: { + dataLabelPosition: + slide?.content.graph.type === "pie" + ? ("Outside" as const) + : ("Inside" as const), + dataLabelAlignment: "Center" as const, + }, + }; }); useEffect(() => { diff --git a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/NewSlide.tsx b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/NewSlide.tsx index ff1b732d..c1ee87b2 100644 --- a/servers/nextjs/app/(presentation-generator)/components/slide_layouts/NewSlide.tsx +++ b/servers/nextjs/app/(presentation-generator)/components/slide_layouts/NewSlide.tsx @@ -140,175 +140,7 @@ const LayoutPreview = ({ type }: { type: string }) => { ) - case 'type10': - return ( -
+ Font Used:
+
+ {getFontLink(currentColors.fontFamily).name || ''}