presenton/app/ipc/slide_metadata.ts

334 lines
11 KiB
TypeScript

import { ipcMain } from "electron";
import puppeteer from "puppeteer";
import fs from 'fs';
import path from 'path';
import { baseDir, isDev, tempDir } from "../utils/constants";
interface Position {
left: number;
top: number;
width: number;
height: number;
}
interface FontStyles {
name: string;
size: number;
bold: boolean;
weight: number;
color: string;
}
interface TextElement {
position: Position;
paragraphs: {
alignment: number;
text: string;
font: FontStyles;
}[];
}
interface PictureElement {
position: Position;
picture: {
is_network: boolean;
path: string;
};
shape: string | null;
object_fit: {
fit: string | null;
focus: number[];
};
overlay: string | null;
border_radius: number[];
}
interface BoxElement {
position: Position;
type: number;
fill: {
color: string;
};
border_radius: number;
stroke: {
color: string;
thickness: number;
};
shadow: {
radius: number;
color: string;
offset: number;
opacity: number;
angle: number;
};
}
interface LineElement {
position: Position;
lineType: number;
thickness: string;
color: string;
}
interface GraphElement {
position: Position;
picture: {
is_network: boolean;
path: string;
};
border_radius: number[];
}
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;
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']
});
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);
});
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 });
// 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("");
};
const slidesMetadata: SlideMetadata[] = [];
const slideContainers = document.querySelectorAll('[data-element-type="slide-container"]');
slideContainers.forEach((container) => {
const containerEl = container as HTMLElement;
containerEl.style.width = "1280px";
containerEl.style.height = "720px";
containerEl.style.transform = "none";
const containerRect = containerEl.getBoundingClientRect();
const slideIndex = parseInt(containerEl.getAttribute("data-slide-index") || "0");
const backgroundColor = rgbToHex(window.getComputedStyle(containerEl).backgroundColor);
const elements: SlideElement[] = [];
const slideElements = containerEl.querySelectorAll('[data-slide-element]:not([data-element-type="slide-container"])');
slideElements.forEach((element) => {
const el = element as HTMLElement;
const elementRect = el.getBoundingClientRect();
const computedStyle = window.getComputedStyle(el);
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),
};
const elementType = el.getAttribute("data-element-type");
if (!elementType) return;
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) {
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),
} as PictureElement);
}
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;
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]);
}
}
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;
}
});
slidesMetadata.push({ slideIndex, backgroundColor, elements });
});
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}`) {
element.picture.path = filePath;
}
});
});
}
return metadata;
} catch (error) {
console.error("Error during page preparation:", error);
throw error;
} finally {
if (browser) await browser.close();
}
});
}