Merge branch 'feat/custom_schema_and_layout' of github.com:presenton/presenton into feat/custom_schema_and_layout

This commit is contained in:
shiva raj badu 2025-07-26 02:43:02 +05:45
commit 5faf61d70f
No known key found for this signature in database
16 changed files with 722 additions and 27 deletions

View file

@ -60,7 +60,7 @@ async def stream_outlines(presentation_id: str):
sql_session.refresh(presentation)
yield SSECompleteResponse(
key="presentation", value=presentation.model_dump_json()
key="presentation", value=presentation.model_dump(mode="json")
).to_string()
return StreamingResponse(inner(), media_type="text/event-stream")

View file

@ -8,7 +8,10 @@ from fastapi import APIRouter, Body, HTTPException
from fastapi.responses import StreamingResponse
from sqlalchemy import delete
from sqlmodel import select
from models.presentation_outline_model import PresentationOutlineModel, SlideOutlineModel
from models.presentation_outline_model import (
PresentationOutlineModel,
SlideOutlineModel,
)
from models.pptx_models import PptxPresentationModel
from models.presentation_layout import PresentationLayoutModel
from models.presentation_structure_model import PresentationStructureModel
@ -304,8 +307,11 @@ async def create_pptx(pptx_model: Annotated[PptxPresentationModel, Body()]):
return pptx_path
@PRESENTATION_ROUTER.post("/generate")
async def generate_presentation_api(data: Annotated[GeneratePresentationRequest, Body()]):
async def generate_presentation_api(
data: Annotated[GeneratePresentationRequest, Body()],
):
presentation_id = str(uuid.uuid4())
print("**" * 40)
print(f"Generating presentation with ID: {presentation_id}")
@ -341,7 +347,7 @@ async def generate_presentation_api(data: Annotated[GeneratePresentationRequest,
presentation_content_json = json.loads(presentation_content_text)
presentation_content = PresentationOutlineModel(**presentation_content_json)
outlines = presentation_content.slides[:data.n_slides]
outlines = presentation_content.slides[: data.n_slides]
total_outlines = len(outlines)
print("-" * 40)
@ -441,7 +447,10 @@ async def generate_presentation_api(data: Annotated[GeneratePresentationRequest,
if response.status != 200:
error_text = await response.text()
print(f"Failed to get PPTX model: {error_text}")
raise HTTPException(status_code=500, detail="Failed to convert presentation to PPTX model")
raise HTTPException(
status_code=500,
detail="Failed to convert presentation to PPTX model",
)
pptx_model_data = await response.json()
print(f"Received PPTX model data: {json.dumps(pptx_model_data, indent=2)}")
@ -453,9 +462,7 @@ async def generate_presentation_api(data: Annotated[GeneratePresentationRequest,
await pptx_creator.create_ppt()
export_directory = get_exports_directory()
pptx_path = os.path.join(
export_directory, f"{presentation_content.title}.pptx"
)
pptx_path = os.path.join(export_directory, f"{presentation_content.title}.pptx")
pptx_creator.save(pptx_path)
presentation_and_path = PresentationAndPath(

View file

@ -132,6 +132,7 @@ class PptxPictureBoxModel(PptxShapeModel):
position: PptxPositionModel
margin: Optional[PptxSpacingModel] = None
clip: bool = True
opacity: Optional[float] = None
overlay: Optional[str] = None
border_radius: Optional[List[int]] = None
shape: Optional[PptxBoxShapeEnum] = None

View file

@ -12,3 +12,4 @@ class SlideModel(SQLModel, table=True):
index: int
content: dict = Field(sa_column=Column(JSON))
html_content: Optional[str]
properties: Optional[dict] = Field(sa_column=Column(JSON))

View file

@ -20,6 +20,15 @@ class SSEStatusResponse(BaseModel):
).to_string()
class SSEErrorResponse(BaseModel):
detail: str
def to_string(self):
return SSEResponse(
event="response", data=json.dumps({"type": "error", "detail": self.detail})
).to_string()
class SSECompleteResponse(BaseModel):
key: str
value: object

View file

@ -39,6 +39,7 @@ from utils.image_utils import (
create_circle_image,
fit_image,
round_image_corners,
set_image_opacity,
)
BLANK_SLIDE_LAYOUT = 6
@ -174,6 +175,7 @@ class PptxPresentationCreator:
picture_model.clip
or picture_model.border_radius
or picture_model.overlay
or picture_model.opacity
or picture_model.object_fit
or picture_model.shape
):
@ -206,6 +208,8 @@ class PptxPresentationCreator:
image = create_circle_image(image)
if picture_model.overlay:
image = change_image_color(image, picture_model.overlay)
if picture_model.opacity:
image = set_image_opacity(image, picture_model.opacity)
image_path = os.path.join(self._temp_dir, f"{str(uuid.uuid4())}.png")
image.save(image_path)

View file

@ -0,0 +1 @@
# async def format_exc(e: Exception):

View file

@ -182,6 +182,28 @@ def create_circle_image(
return result
def set_image_opacity(image: Image.Image, opacity: float) -> Image.Image:
# Clamp opacity to valid range
opacity = max(0.0, min(1.0, opacity))
# Convert to RGBA if not already
if image.mode != "RGBA":
image = image.convert("RGBA")
# Get the original alpha channel
original_alpha = image.getchannel("A")
# Create new alpha channel with adjusted opacity
new_alpha = original_alpha.point(lambda x: int(x * opacity))
# Create new image with modified alpha channel
result = Image.new("RGBA", image.size)
result.paste(image.convert("RGB"), (0, 0))
result.putalpha(new_alpha)
return result
def fit_image(
image: Image.Image, width: int, height: int, object_fit: PptxObjectFitModel
) -> Image.Image:

View file

@ -22,7 +22,7 @@ export async function POST(req: NextRequest) {
const page = await browser.newPage();
await page.setViewport({ width: 1280, height: 720 });
await page.goto(`http://localhost/pdf-maker?id=${id}`, { waitUntil: 'networkidle0', timeout: 80000 });
await page.goto(`http://localhost/pdf-maker?id=${id}`, { waitUntil: 'networkidle0', timeout: 180000 });
await page.waitForFunction('() => document.readyState === "complete"')

View file

@ -7,6 +7,7 @@ import { PptxPresentationModel } from "@/types/pptx_models";
import fs from "fs";
import path from "path";
import { v4 as uuidv4 } from 'uuid';
import sharp from "sharp";
interface GetAllChildElementsAttributesArgs {
element: ElementHandle<Element>;
@ -16,6 +17,7 @@ interface GetAllChildElementsAttributesArgs {
inheritedBackground?: ElementAttributes['background'];
inheritedBorderRadius?: number[];
inheritedZIndex?: number;
inheritedOpacity?: number;
screenshotsDir: string;
}
@ -75,7 +77,7 @@ async function getBrowserAndPage(id: string): Promise<[Browser, Page]> {
await page.setViewport({ width: 1280, height: 720, deviceScaleFactor: 1 });
await page.goto(`http://localhost/pdf-maker?id=${id}`, {
waitUntil: "networkidle0",
timeout: 60000,
timeout: 180000,
});
return [browser, page];
}
@ -115,6 +117,13 @@ async function postProcessSlidesAttributes(slidesAttributes: SlideAttributesResu
async function screenshotElement(element: ElementAttributes, screenshotsDir: string) {
const screenshotPath = path.join(screenshotsDir, `${uuidv4()}.png`) as `${string}.png`;
// For SVG elements, use convertSvgToPng
if (element.tagName === 'svg') {
const pngBuffer = await convertSvgToPng(element);
fs.writeFileSync(screenshotPath, pngBuffer);
return screenshotPath;
}
// Hide all elements except the target element and its ancestors
await element.element?.evaluate((el) => {
const originalOpacities = new Map();
@ -137,12 +146,12 @@ async function screenshotElement(element: ElementAttributes, screenshotsDir: str
hideAllExcept(el);
(el as any).__restoreOpacities = () => {
(el as any).__restoreStyles = () => {
originalOpacities.forEach((opacity, elem) => {
(elem as HTMLElement).style.opacity = opacity;
});
};
});
}, element.opacity, element.font?.color);
const screenshot = await element.element?.screenshot({ path: screenshotPath });
if (!screenshot) {
@ -150,14 +159,32 @@ async function screenshotElement(element: ElementAttributes, screenshotsDir: str
}
await element.element?.evaluate((el) => {
if ((el as any).__restoreOpacities) {
(el as any).__restoreOpacities();
if ((el as any).__restoreStyles) {
(el as any).__restoreStyles();
}
});
return screenshotPath;
}
const convertSvgToPng = async (element_attibutes: ElementAttributes) => {
const svgHtml = await element_attibutes.element?.evaluate((el) => {
// Apply font color
const fontColor = window.getComputedStyle(el).color;
(el as HTMLElement).style.color = fontColor;
return el.outerHTML;
}) || '';
const svgBuffer = Buffer.from(svgHtml);
const pngBuffer = await sharp(svgBuffer)
.resize(Math.round(element_attibutes.position?.width ?? 10), Math.round(element_attibutes.position?.height ?? 10))
.toFormat('png')
.toBuffer();
return pngBuffer;
}
async function getSlidesAttributes(slides: ElementHandle<Element>[], screenshotsDir: string): Promise<SlideAttributesResult[]> {
const slideAttributes = await Promise.all(
@ -182,12 +209,13 @@ async function getSlidesWrapper(page: Page): Promise<ElementHandle<Element>> {
return slides_wrapper;
}
async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground, inheritedBorderRadius, inheritedZIndex, screenshotsDir }: GetAllChildElementsAttributesArgs): Promise<SlideAttributesResult> {
async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground, inheritedBorderRadius, inheritedZIndex, inheritedOpacity, screenshotsDir }: GetAllChildElementsAttributesArgs): Promise<SlideAttributesResult> {
if (!rootRect) {
const rootAttributes = await getElementAttributes(element);
inheritedFont = rootAttributes.font;
inheritedBackground = rootAttributes.background;
inheritedZIndex = rootAttributes.zIndex;
inheritedOpacity = rootAttributes.opacity;
rootRect = {
left: rootAttributes.position?.left ?? 0,
top: rootAttributes.position?.top ?? 0,
@ -220,6 +248,9 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
if (inheritedZIndex !== undefined && attributes.zIndex === 0) {
attributes.zIndex = inheritedZIndex;
}
if (inheritedOpacity !== undefined && (attributes.opacity === undefined || attributes.opacity === 1)) {
attributes.opacity = inheritedOpacity;
}
if (attributes.position && attributes.position.left !== undefined && attributes.position.top !== undefined) {
attributes.position = {
@ -251,6 +282,7 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
inheritedBackground: attributes.background || inheritedBackground,
inheritedBorderRadius: attributes.borderRadius || inheritedBorderRadius,
inheritedZIndex: attributes.zIndex || inheritedZIndex,
inheritedOpacity: attributes.opacity || inheritedOpacity,
screenshotsDir,
});
allResults.push(...childResults.elements.map(attr => ({ attributes: attr, depth: depth + 1 })));
@ -284,14 +316,16 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
const isCanvas = attributes.tagName === 'canvas';
const isTable = attributes.tagName === 'table';
const isRootPosition = attributes.position &&
const occupiesRoot = attributes.position &&
attributes.position.left === 0 &&
attributes.position.top === 0 &&
attributes.position.width === rootRect!.width &&
attributes.position.height === rootRect!.height;
const hasOtherProperties = hasBackground || hasBorder || hasShadow || hasText || hasImage || isSvg || isCanvas || isTable;
return hasOtherProperties && !isRootPosition;
const hasVisualProperties = hasBackground || hasBorder || hasShadow || hasText;
const hasSpecialContent = hasImage || isSvg || isCanvas || isTable;
return (hasVisualProperties && !occupiesRoot) || hasSpecialContent;
}) : allResults;
if (depth === 0) {
@ -430,6 +464,23 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
return background;
}
function parseBackgroundImage(computedStyles: CSSStyleDeclaration) {
const backgroundImage = computedStyles.backgroundImage;
if (!backgroundImage || backgroundImage === 'none') {
return undefined;
}
// Extract URL from background-image style
const urlMatch = backgroundImage.match(/url\(['"]?([^'"]+)['"]?\)/);
if (urlMatch && urlMatch[1]) {
return urlMatch[1];
}
return undefined;
}
function parseBorder(computedStyles: CSSStyleDeclaration) {
const borderColorResult = colorToHex(computedStyles.borderColor);
const borderWidth = parseFloat(computedStyles.borderWidth);
@ -783,7 +834,75 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
return undefined;
}
function parseFilters(computedStyles: CSSStyleDeclaration) {
const filter = computedStyles.filter;
if (!filter || filter === 'none') {
return undefined;
}
const filters: {
invert?: number;
brightness?: number;
contrast?: number;
saturate?: number;
hueRotate?: number;
blur?: number;
grayscale?: number;
sepia?: number;
opacity?: number;
} = {};
// Parse filter functions
const filterFunctions = filter.match(/[a-zA-Z]+\([^)]*\)/g);
if (filterFunctions) {
filterFunctions.forEach(func => {
const match = func.match(/([a-zA-Z]+)\(([^)]*)\)/);
if (match) {
const filterType = match[1];
const value = parseFloat(match[2]);
if (!isNaN(value)) {
switch (filterType) {
case 'invert':
filters.invert = value;
break;
case 'brightness':
filters.brightness = value;
break;
case 'contrast':
filters.contrast = value;
break;
case 'saturate':
filters.saturate = value;
break;
case 'hue-rotate':
filters.hueRotate = value;
break;
case 'blur':
filters.blur = value;
break;
case 'grayscale':
filters.grayscale = value;
break;
case 'sepia':
filters.sepia = value;
break;
case 'opacity':
filters.opacity = value;
break;
}
}
}
});
}
// Return undefined if no filters were parsed
return Object.keys(filters).length > 0 ? filters : undefined;
}
function parseElementAttributes(el: Element) {
let tagName = el.tagName.toLowerCase();
const computedStyles = window.getComputedStyle(el);
const position = parsePosition(el);
@ -809,7 +928,9 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
const textAlign = computedStyles.textAlign as 'left' | 'center' | 'right' | 'justify';
const objectFit = computedStyles.objectFit as 'contain' | 'cover' | 'fill' | undefined;
const imageSrc = (el as HTMLImageElement).src;
const parsedBackgroundImage = parseBackgroundImage(computedStyles);
const imageSrc = (el as HTMLImageElement).src || parsedBackgroundImage;
const borderRadiusValue = parseBorderRadius(computedStyles, el);
@ -817,11 +938,17 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
const textWrap = computedStyles.whiteSpace !== 'nowrap';
const filters = parseFilters(computedStyles);
const opacity = parseFloat(computedStyles.opacity);
const elementOpacity = isNaN(opacity) ? undefined : opacity;
return {
tagName: el.tagName.toLowerCase(),
tagName: tagName,
id: el.id,
className: (el.className && typeof el.className === 'string') ? el.className : (el.className ? el.className.toString() : undefined),
innerText: innerText,
opacity: elementOpacity,
background: background,
border: border,
shadow: shadow,
@ -842,6 +969,7 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
textWrap: textWrap,
should_screenshot: false,
element: undefined,
filters: filters,
};
}

View file

@ -35,6 +35,7 @@
"@tiptap/extension-underline": "^2.0.0",
"@tiptap/react": "^2.11.5",
"@tiptap/starter-kit": "^2.11.5",
"@types/sharp": "^0.32.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@ -48,6 +49,7 @@
"react-dom": "^18",
"react-redux": "^9.1.2",
"recharts": "^2.15.4",
"sharp": "^0.34.3",
"sonner": "^2.0.6",
"tailwind-merge": "^2.5.3",
"tailwind-scrollbar-hide": "^2.0.0",
@ -229,6 +231,15 @@
"react": ">=16.8.0"
}
},
"node_modules/@emnapi/runtime": {
"version": "1.4.5",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz",
"integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==",
"optional": true,
"dependencies": {
"tslib": "^2.4.0"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.7.2.tgz",
@ -267,6 +278,402 @@
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@img/sharp-darwin-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.3.tgz",
"integrity": "sha512-ryFMfvxxpQRsgZJqBd4wsttYQbCxsJksrv9Lw/v798JcQ8+w84mBWuXwl+TT0WJ/WrYOLaYpwQXi3sA9nTIaIg==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-darwin-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.3.tgz",
"integrity": "sha512-yHpJYynROAj12TA6qil58hmPmAwxKKC7reUqtGLzsOHfP7/rniNGTL8tjWX6L3CTV4+5P4ypcS7Pp+7OB+8ihA==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-darwin-x64": "1.2.0"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.0.tgz",
"integrity": "sha512-sBZmpwmxqwlqG9ueWFXtockhsxefaV6O84BMOrhtg/YqbTaRdqDE7hxraVE3y6gVM4eExmfzW4a8el9ArLeEiQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.0.tgz",
"integrity": "sha512-M64XVuL94OgiNHa5/m2YvEQI5q2cl9d/wk0qFTDVXcYzi43lxuiFTftMR1tOnFQovVXNZJ5TURSDK2pNe9Yzqg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"darwin"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.0.tgz",
"integrity": "sha512-mWd2uWvDtL/nvIzThLq3fr2nnGfyr/XMXlq8ZJ9WMR6PXijHlC3ksp0IpuhK6bougvQrchUAfzRLnbsen0Cqvw==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.0.tgz",
"integrity": "sha512-RXwd0CgG+uPRX5YYrkzKyalt2OJYRiJQ8ED/fi1tq9WQW2jsQIn0tqrlR5l5dr/rjqq6AHAxURhj2DVjyQWSOA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.0.tgz",
"integrity": "sha512-Xod/7KaDDHkYu2phxxfeEPXfVXFKx70EAFZ0qyUdOjCcxbjqyJOEUpDe6RIyaunGxT34Anf9ue/wuWOqBW2WcQ==",
"cpu": [
"ppc64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.0.tgz",
"integrity": "sha512-eMKfzDxLGT8mnmPJTNMcjfO33fLiTDsrMlUVcp6b96ETbnJmd4uvZxVJSKPQfS+odwfVaGifhsB07J1LynFehw==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.0.tgz",
"integrity": "sha512-ZW3FPWIc7K1sH9E3nxIGB3y3dZkpJlMnkk7z5tu1nSkBoCgw2nSRTFHI5pB/3CQaJM0pdzMF3paf9ckKMSE9Tg==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.0.tgz",
"integrity": "sha512-UG+LqQJbf5VJ8NWJ5Z3tdIe/HXjuIdo4JeVNADXBFuG7z9zjoegpzzGIyV5zQKi4zaJjnAd2+g2nna8TZvuW9Q==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.0.tgz",
"integrity": "sha512-SRYOLR7CXPgNze8akZwjoGBoN1ThNZoqpOgfnOxmWsklTGVfJiGJoC/Lod7aNMGA1jSsKWM1+HRX43OP6p9+6Q==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-linux-arm": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.3.tgz",
"integrity": "sha512-oBK9l+h6KBN0i3dC8rYntLiVfW8D8wH+NPNT3O/WBHeW0OQWCjfWksLUaPidsrDKpJgXp3G3/hkmhptAW0I3+A==",
"cpu": [
"arm"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm": "1.2.0"
}
},
"node_modules/@img/sharp-linux-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.3.tgz",
"integrity": "sha512-QdrKe3EvQrqwkDrtuTIjI0bu6YEJHTgEeqdzI3uWJOH6G1O8Nl1iEeVYRGdj1h5I21CqxSvQp1Yv7xeU3ZewbA==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-linux-ppc64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.3.tgz",
"integrity": "sha512-GLtbLQMCNC5nxuImPR2+RgrviwKwVql28FWZIW1zWruy6zLgA5/x2ZXk3mxj58X/tszVF69KK0Is83V8YgWhLA==",
"cpu": [
"ppc64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-ppc64": "1.2.0"
}
},
"node_modules/@img/sharp-linux-s390x": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.3.tgz",
"integrity": "sha512-3gahT+A6c4cdc2edhsLHmIOXMb17ltffJlxR0aC2VPZfwKoTGZec6u5GrFgdR7ciJSsHT27BD3TIuGcuRT0KmQ==",
"cpu": [
"s390x"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-s390x": "1.2.0"
}
},
"node_modules/@img/sharp-linux-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.3.tgz",
"integrity": "sha512-8kYso8d806ypnSq3/Ly0QEw90V5ZoHh10yH0HnrzOCr6DKAPI6QVHvwleqMkVQ0m+fc7EH8ah0BB0QPuWY6zJQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linux-x64": "1.2.0"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.3.tgz",
"integrity": "sha512-vAjbHDlr4izEiXM1OTggpCcPg9tn4YriK5vAjowJsHwdBIdx0fYRsURkxLG2RLm9gyBq66gwtWI8Gx0/ov+JKQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.3.tgz",
"integrity": "sha512-gCWUn9547K5bwvOn9l5XGAEjVTTRji4aPTqLzGXHvIr6bIDZKNTA34seMPgM0WmSf+RYBH411VavCejp3PkOeQ==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"linux"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-libvips-linuxmusl-x64": "1.2.0"
}
},
"node_modules/@img/sharp-wasm32": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.3.tgz",
"integrity": "sha512-+CyRcpagHMGteySaWos8IbnXcHgfDn7pO2fiC2slJxvNq9gDipYBN42/RagzctVRKgxATmfqOSulgZv5e1RdMg==",
"cpu": [
"wasm32"
],
"optional": true,
"dependencies": {
"@emnapi/runtime": "^1.4.4"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.3.tgz",
"integrity": "sha512-MjnHPnbqMXNC2UgeLJtX4XqoVHHlZNd+nPt1kRPmj63wURegwBhZlApELdtxM2OIZDRv/DFtLcNhVbd1z8GYXQ==",
"cpu": [
"arm64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.3.tgz",
"integrity": "sha512-xuCdhH44WxuXgOM714hn4amodJMZl3OEvf0GVTm0BEyMeA2to+8HEdRPShH0SLYptJY1uBw+SCFP9WVQi1Q/cw==",
"cpu": [
"ia32"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.3.tgz",
"integrity": "sha512-OWwz05d++TxzLEv4VnsTz5CmZ6mI6S05sfQGEMrNrQcOEERbX46332IvE7pO/EUiw7jUrrS40z/M7kPyjfl04g==",
"cpu": [
"x64"
],
"optional": true,
"os": [
"win32"
],
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@isaacs/cliui": {
"version": "8.0.2",
"resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
@ -2369,6 +2776,15 @@
"@types/react": "^18.0.0"
}
},
"node_modules/@types/sharp": {
"version": "0.32.0",
"resolved": "https://registry.npmjs.org/@types/sharp/-/sharp-0.32.0.tgz",
"integrity": "sha512-OOi3kL+FZDnPhVzsfD37J88FNeZh6gQsGcLc95NbeURRGvmSjeXiDcyWzF2o3yh/gQAUn2uhh/e+CPCa5nwAxw==",
"deprecated": "This is a stub types definition. sharp provides its own type definitions, so you do not need this installed.",
"dependencies": {
"sharp": "*"
}
},
"node_modules/@types/sinonjs__fake-timers": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/@types/sinonjs__fake-timers/-/sinonjs__fake-timers-8.1.1.tgz",
@ -3176,6 +3592,18 @@
"react-dom": "^18 || ^19 || ^19.0.0-rc"
}
},
"node_modules/color": {
"version": "4.2.3",
"resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz",
"integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==",
"dependencies": {
"color-convert": "^2.0.1",
"color-string": "^1.9.0"
},
"engines": {
"node": ">=12.5.0"
}
},
"node_modules/color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
@ -3194,6 +3622,15 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"license": "MIT"
},
"node_modules/color-string": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz",
"integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==",
"dependencies": {
"color-name": "^1.0.0",
"simple-swizzle": "^0.2.2"
}
},
"node_modules/colorette": {
"version": "2.0.20",
"resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz",
@ -3573,6 +4010,14 @@
"node": ">=0.4.0"
}
},
"node_modules/detect-libc": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"engines": {
"node": ">=8"
}
},
"node_modules/detect-node-es": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz",
@ -6342,6 +6787,47 @@
"node": ">=10"
}
},
"node_modules/sharp": {
"version": "0.34.3",
"resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.3.tgz",
"integrity": "sha512-eX2IQ6nFohW4DbvHIOLRB3MHFpYqaqvXd3Tp5e/T/dSH83fxaNJQRvDMhASmkNTsNTVF2/OOopzRCt7xokgPfg==",
"hasInstallScript": true,
"dependencies": {
"color": "^4.2.3",
"detect-libc": "^2.0.4",
"semver": "^7.7.2"
},
"engines": {
"node": "^18.17.0 || ^20.3.0 || >=21.0.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
"@img/sharp-darwin-arm64": "0.34.3",
"@img/sharp-darwin-x64": "0.34.3",
"@img/sharp-libvips-darwin-arm64": "1.2.0",
"@img/sharp-libvips-darwin-x64": "1.2.0",
"@img/sharp-libvips-linux-arm": "1.2.0",
"@img/sharp-libvips-linux-arm64": "1.2.0",
"@img/sharp-libvips-linux-ppc64": "1.2.0",
"@img/sharp-libvips-linux-s390x": "1.2.0",
"@img/sharp-libvips-linux-x64": "1.2.0",
"@img/sharp-libvips-linuxmusl-arm64": "1.2.0",
"@img/sharp-libvips-linuxmusl-x64": "1.2.0",
"@img/sharp-linux-arm": "0.34.3",
"@img/sharp-linux-arm64": "0.34.3",
"@img/sharp-linux-ppc64": "0.34.3",
"@img/sharp-linux-s390x": "0.34.3",
"@img/sharp-linux-x64": "0.34.3",
"@img/sharp-linuxmusl-arm64": "0.34.3",
"@img/sharp-linuxmusl-x64": "0.34.3",
"@img/sharp-wasm32": "0.34.3",
"@img/sharp-win32-arm64": "0.34.3",
"@img/sharp-win32-ia32": "0.34.3",
"@img/sharp-win32-x64": "0.34.3"
}
},
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@ -6446,6 +6932,19 @@
"dev": true,
"license": "ISC"
},
"node_modules/simple-swizzle": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz",
"integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==",
"dependencies": {
"is-arrayish": "^0.3.1"
}
},
"node_modules/simple-swizzle/node_modules/is-arrayish": {
"version": "0.3.2",
"resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz",
"integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="
},
"node_modules/slice-ansi": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-3.0.0.tgz",

View file

@ -37,6 +37,7 @@
"@tiptap/extension-underline": "^2.0.0",
"@tiptap/react": "^2.11.5",
"@tiptap/starter-kit": "^2.11.5",
"@types/sharp": "^0.32.0",
"class-variance-authority": "^0.7.0",
"clsx": "^2.1.1",
"cmdk": "^1.0.0",
@ -50,6 +51,7 @@
"react-dom": "^18",
"react-redux": "^9.1.2",
"recharts": "^2.15.4",
"sharp": "^0.34.3",
"sonner": "^2.0.6",
"tailwind-merge": "^2.5.3",
"tailwind-scrollbar-hide": "^2.0.0",

View file

@ -48,7 +48,7 @@ const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data: slideData })
}}
>
{/* Background Image */}
<div
<div
className="absolute inset-0 w-full h-full bg-cover bg-center bg-no-repeat"
style={{
backgroundImage: `url('${slideData?.backgroundImage?.__image_url__ || ''}')`,
@ -66,7 +66,7 @@ const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data: slideData })
{/* Main Content */}
<div className="relative z-10 px-8 sm:px-12 lg:px-20 py-12 flex-1 flex flex-col justify-center h-full">
<div className="text-center space-y-8 max-w-4xl mx-auto">
{/* Heading */}
<div className="space-y-4">
<h1 className="text-3xl sm:text-4xl lg:text-5xl font-bold text-white leading-tight">
@ -80,12 +80,12 @@ const QuoteSlideLayout: React.FC<QuoteSlideLayoutProps> = ({ data: slideData })
<div className="space-y-6">
{/* Quote Icon */}
<div className="flex justify-center">
<svg
className="w-12 h-12 text-purple-300 opacity-80"
fill="currentColor"
<svg
className="w-12 h-12 text-purple-300 opacity-80"
fill="currentColor"
viewBox="0 0 24 24"
>
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"/>
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z" />
</svg>
</div>

View file

@ -5,6 +5,7 @@ export interface ElementAttributes {
id?: string;
className?: string;
innerText?: string;
opacity?: number;
background?: {
color?: string;
opacity?: number;
@ -61,6 +62,17 @@ export interface ElementAttributes {
textWrap?: boolean;
should_screenshot?: boolean;
element?: ElementHandle<Element>;
filters?: {
invert?: number;
brightness?: number;
contrast?: number;
saturate?: number;
hueRotate?: number;
blur?: number;
grayscale?: number;
sepia?: number;
opacity?: number;
};
}
export interface SlideAttributesResult {

View file

@ -305,6 +305,7 @@ export interface PptxPictureBoxModel extends PptxShapeModel {
margin?: PptxSpacingModel;
clip: boolean;
overlay?: string;
opacity?: number;
border_radius?: number[];
shape?: PptxBoxShapeEnum;
object_fit?: PptxObjectFitModel;

View file

@ -78,6 +78,7 @@ export function convertElementAttributesToPptxSlides(
function convertElementToPptxShape(
element: ElementAttributes
): PptxTextBoxModel | PptxAutoShapeBoxModel | PptxConnectorModel | PptxPictureBoxModel | null {
if (!element.position) {
return null;
}
@ -218,11 +219,18 @@ function convertToPictureBox(element: ElementAttributes): PptxPictureBoxModel {
path: element.imageSrc || ''
};
// Set overlay to white if invert is 1 and brightness is 0
let overlay = element.overlay;
if (element.filters?.invert === 1 && element.filters?.brightness === 0) {
overlay = 'FFFFFF';
}
return {
position,
margin: undefined,
clip: element.clip ?? true,
overlay: element.overlay,
overlay,
opacity: element.opacity,
border_radius: element.borderRadius ? element.borderRadius.map(r => Math.round(r)) : undefined,
shape: element.shape ? (element.shape as PptxBoxShapeEnum) : PptxBoxShapeEnum.RECTANGLE,
object_fit: objectFit,