334 lines
11 KiB
TypeScript
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();
|
|
}
|
|
});
|
|
}
|