Merge branch 'feat/custom_schema_and_layout' of github.com:presenton/presenton into feat/custom_schema_and_layout
This commit is contained in:
commit
8dfd8734c0
8 changed files with 222 additions and 143 deletions
|
|
@ -7,7 +7,7 @@ from utils.get_env import get_app_data_directory_env, get_database_url_env
|
|||
|
||||
|
||||
database_url = get_database_url_env() or "sqlite:///" + os.path.join(
|
||||
get_app_data_directory_env(), "fastapi.db"
|
||||
get_app_data_directory_env() or "/tmp/presenton", "fastapi.db"
|
||||
)
|
||||
connect_args = {}
|
||||
if "sqlite" in database_url:
|
||||
|
|
|
|||
|
|
@ -404,43 +404,76 @@ class PptxPresentationCreator:
|
|||
# # Remove existing shadow effects if present
|
||||
effect_list = sp_pr.find("a:effectLst", namespaces=nsmap)
|
||||
if effect_list:
|
||||
old_shadow = effect_list.find("a:outerShdw")
|
||||
if old_shadow:
|
||||
old_outer_shadow = effect_list.find("a:outerShdw")
|
||||
if old_outer_shadow:
|
||||
effect_list.remove(
|
||||
old_shadow, namespaces=nsmap
|
||||
old_outer_shadow, namespaces=nsmap
|
||||
) # Remove the old shadow
|
||||
old_inner_shadow = effect_list.find("a:innerShdw")
|
||||
if old_inner_shadow:
|
||||
effect_list.remove(
|
||||
old_inner_shadow, namespaces=nsmap
|
||||
) # Remove the old shadow
|
||||
old_prst_shadow = effect_list.find("a:prstShdw")
|
||||
if old_prst_shadow:
|
||||
effect_list.remove(
|
||||
old_prst_shadow, namespaces=nsmap
|
||||
) # Remove the old shadow
|
||||
|
||||
if not shadow:
|
||||
return
|
||||
|
||||
if not effect_list:
|
||||
effect_list = etree.SubElement(
|
||||
sp_pr, f"{{{nsmap['a']}}}effectLst", nsmap=nsmap
|
||||
)
|
||||
|
||||
outer_shadow = etree.SubElement(
|
||||
effect_list,
|
||||
f"{{{nsmap['a']}}}outerShdw",
|
||||
{
|
||||
"blurRad": f"{Pt(shadow.radius)}",
|
||||
"dir": f"{shadow.angle * 1000}",
|
||||
"dist": f"{Pt(shadow.offset)}",
|
||||
"rotWithShape": "0",
|
||||
},
|
||||
nsmap=nsmap,
|
||||
)
|
||||
color_element = etree.SubElement(
|
||||
outer_shadow,
|
||||
f"{{{nsmap['a']}}}srgbClr",
|
||||
{"val": f"{shadow.color}"},
|
||||
nsmap=nsmap,
|
||||
)
|
||||
etree.SubElement(
|
||||
color_element,
|
||||
f"{{{nsmap['a']}}}alpha",
|
||||
{"val": f"{int(shadow.opacity * 100000)}"},
|
||||
nsmap=nsmap,
|
||||
)
|
||||
if shadow is None:
|
||||
# Apply shadow with zero values when shadow is None
|
||||
outer_shadow = etree.SubElement(
|
||||
effect_list,
|
||||
f"{{{nsmap['a']}}}outerShdw",
|
||||
{
|
||||
"blurRad": "0",
|
||||
"dist": "0",
|
||||
"dir": "0",
|
||||
},
|
||||
nsmap=nsmap,
|
||||
)
|
||||
color_element = etree.SubElement(
|
||||
outer_shadow,
|
||||
f"{{{nsmap['a']}}}srgbClr",
|
||||
{"val": "000000"},
|
||||
nsmap=nsmap,
|
||||
)
|
||||
etree.SubElement(
|
||||
color_element,
|
||||
f"{{{nsmap['a']}}}alpha",
|
||||
{"val": "0"},
|
||||
nsmap=nsmap,
|
||||
)
|
||||
else:
|
||||
# Apply the provided shadow
|
||||
outer_shadow = etree.SubElement(
|
||||
effect_list,
|
||||
f"{{{nsmap['a']}}}outerShdw",
|
||||
{
|
||||
"blurRad": f"{Pt(shadow.radius)}",
|
||||
"dir": f"{shadow.angle * 1000}",
|
||||
"dist": f"{Pt(shadow.offset)}",
|
||||
"rotWithShape": "0",
|
||||
},
|
||||
nsmap=nsmap,
|
||||
)
|
||||
color_element = etree.SubElement(
|
||||
outer_shadow,
|
||||
f"{{{nsmap['a']}}}srgbClr",
|
||||
{"val": f"{shadow.color}"},
|
||||
nsmap=nsmap,
|
||||
)
|
||||
etree.SubElement(
|
||||
color_element,
|
||||
f"{{{nsmap['a']}}}alpha",
|
||||
{"val": f"{int(shadow.opacity * 100000)}"},
|
||||
nsmap=nsmap,
|
||||
)
|
||||
|
||||
def set_fill_opacity(self, fill, opacity):
|
||||
if opacity is None or opacity >= 1.0:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ from utils.get_env import get_temp_directory_env
|
|||
class TempFileService:
|
||||
|
||||
def __init__(self):
|
||||
self.base_dir = get_temp_directory_env()
|
||||
self.base_dir = get_temp_directory_env() or "/tmp/presenton"
|
||||
# TODO: Uncomment this when we want to cleanup the base dir on startup
|
||||
# self.cleanup_base_dir()
|
||||
os.makedirs(self.base_dir, exist_ok=True)
|
||||
|
|
|
|||
40
servers/fastapi/tests/test_pptx_creator.py
Normal file
40
servers/fastapi/tests/test_pptx_creator.py
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
import asyncio
|
||||
from models.pptx_models import (
|
||||
PptxAutoShapeBoxModel,
|
||||
PptxFillModel,
|
||||
PptxPositionModel,
|
||||
PptxPresentationModel,
|
||||
PptxSlideModel,
|
||||
)
|
||||
from services.pptx_presentation_creator import PptxPresentationCreator
|
||||
from pptx.enum.shapes import MSO_AUTO_SHAPE_TYPE
|
||||
|
||||
|
||||
pptx_model = PptxPresentationModel(
|
||||
slides=[
|
||||
PptxSlideModel(
|
||||
shapes=[
|
||||
PptxAutoShapeBoxModel(
|
||||
type=MSO_AUTO_SHAPE_TYPE.RECTANGLE,
|
||||
position=PptxPositionModel(
|
||||
left=20,
|
||||
right=20,
|
||||
width=100,
|
||||
height=100,
|
||||
),
|
||||
fill=PptxFillModel(
|
||||
color="000000",
|
||||
opacity=0.5,
|
||||
),
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
def test_pptx_creator():
|
||||
temp_dir = "/tmp/presenton"
|
||||
pptx_creator = PptxPresentationCreator(pptx_model, temp_dir)
|
||||
asyncio.run(pptx_creator.create_ppt())
|
||||
pptx_creator.save("debug/test.pptx")
|
||||
|
|
@ -108,7 +108,7 @@ export function OutlineItem({
|
|||
{isStreaming ? <textarea
|
||||
defaultValue={slideOutline.body || ''}
|
||||
onBlur={(e) => handleSlideChange({ ...slideOutline, body: e.target.value })}
|
||||
className="text-sm flex-1 font-normal bg-transparent outline-none"
|
||||
className="text-sm flex-1 font-normal bg-transparent outline-none overflow-y-hidden"
|
||||
placeholder="Content goes here"
|
||||
/> : <MarkdownEditor
|
||||
key={index}
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ export async function POST(req: NextRequest) {
|
|||
}
|
||||
}
|
||||
|
||||
return (loadedElements / totalElements) >= 0.95;
|
||||
return (loadedElements / totalElements) >= 0.99;
|
||||
}
|
||||
`,
|
||||
{ timeout: 10000 }
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ interface GetAllChildElementsAttributesArgs {
|
|||
inheritedFont?: ElementAttributes['font'];
|
||||
inheritedBackground?: ElementAttributes['background'];
|
||||
inheritedBorderRadius?: number[];
|
||||
inheritedZIndex?: number;
|
||||
screenshotsDir: string;
|
||||
}
|
||||
|
||||
|
|
@ -181,16 +182,20 @@ async function getSlidesWrapper(page: Page): Promise<ElementHandle<Element>> {
|
|||
return slides_wrapper;
|
||||
}
|
||||
|
||||
async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground, inheritedBorderRadius, screenshotsDir }: GetAllChildElementsAttributesArgs): Promise<SlideAttributesResult> {
|
||||
const currentRootRect = rootRect || await element.evaluate((el) => {
|
||||
const rect = el.getBoundingClientRect();
|
||||
return {
|
||||
left: isFinite(rect.left) ? rect.left : 0,
|
||||
top: isFinite(rect.top) ? rect.top : 0,
|
||||
width: isFinite(rect.width) ? rect.width : 0,
|
||||
height: isFinite(rect.height) ? rect.height : 0,
|
||||
async function getAllChildElementsAttributes({ element, rootRect = null, depth = 0, inheritedFont, inheritedBackground, inheritedBorderRadius, inheritedZIndex, screenshotsDir }: GetAllChildElementsAttributesArgs): Promise<SlideAttributesResult> {
|
||||
if (!rootRect) {
|
||||
const rootAttributes = await getElementAttributes(element);
|
||||
inheritedFont = rootAttributes.font;
|
||||
inheritedBackground = rootAttributes.background;
|
||||
inheritedZIndex = rootAttributes.zIndex;
|
||||
rootRect = {
|
||||
left: rootAttributes.position?.left ?? 0,
|
||||
top: rootAttributes.position?.top ?? 0,
|
||||
width: rootAttributes.position?.width ?? 1280,
|
||||
height: rootAttributes.position?.height ?? 720,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
const directChildElementHandles = await element.$$(':scope > *');
|
||||
|
||||
|
|
@ -199,6 +204,10 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
|
|||
for (const childElementHandle of directChildElementHandles) {
|
||||
const attributes = await getElementAttributes(childElementHandle);
|
||||
|
||||
if (attributes.tagName === "style") {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (inheritedFont && !attributes.font && attributes.innerText && attributes.innerText.trim().length > 0) {
|
||||
attributes.font = inheritedFont;
|
||||
}
|
||||
|
|
@ -208,11 +217,14 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
|
|||
if (inheritedBorderRadius && !attributes.borderRadius) {
|
||||
attributes.borderRadius = inheritedBorderRadius;
|
||||
}
|
||||
if (inheritedZIndex !== undefined && attributes.zIndex === 0) {
|
||||
attributes.zIndex = inheritedZIndex;
|
||||
}
|
||||
|
||||
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,
|
||||
left: attributes.position.left - rootRect!.left,
|
||||
top: attributes.position.top - rootRect!.top,
|
||||
width: attributes.position.width,
|
||||
height: attributes.position.height,
|
||||
};
|
||||
|
|
@ -227,30 +239,31 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
|
|||
|
||||
//? If the element is a svg, canvas, or table, we don't need to go deeper
|
||||
if (attributes.should_screenshot) {
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
const childResults = await getAllChildElementsAttributes({
|
||||
element: childElementHandle,
|
||||
rootRect: currentRootRect,
|
||||
rootRect: rootRect,
|
||||
depth: depth + 1,
|
||||
inheritedFont: attributes.font || inheritedFont,
|
||||
inheritedBackground: attributes.background || inheritedBackground,
|
||||
inheritedBorderRadius: attributes.borderRadius || inheritedBorderRadius,
|
||||
inheritedZIndex: attributes.zIndex || inheritedZIndex,
|
||||
screenshotsDir,
|
||||
});
|
||||
allResults.push(...childResults.elements.map(attr => ({ attributes: attr, depth: depth + 1 })));
|
||||
}
|
||||
|
||||
let backgroundColor: string | undefined;
|
||||
if (!rootRect) {
|
||||
let backgroundColor = inheritedBackground?.color;
|
||||
if (depth === 0) {
|
||||
const elementsWithRootPosition = allResults.filter(({ attributes }) => {
|
||||
return attributes.position &&
|
||||
attributes.position.left === 0 &&
|
||||
attributes.position.top === 0 &&
|
||||
attributes.position.width === currentRootRect.width &&
|
||||
attributes.position.height === currentRootRect.height;
|
||||
attributes.position.width === rootRect!.width &&
|
||||
attributes.position.height === rootRect!.height;
|
||||
});
|
||||
|
||||
for (const { attributes } of elementsWithRootPosition) {
|
||||
|
|
@ -261,7 +274,7 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
|
|||
}
|
||||
}
|
||||
|
||||
const filteredResults = !rootRect ? allResults.filter(({ attributes }) => {
|
||||
const filteredResults = depth === 0 ? allResults.filter(({ attributes }) => {
|
||||
const hasBackground = attributes.background && attributes.background.color;
|
||||
const hasBorder = attributes.border && attributes.border.color;
|
||||
const hasShadow = attributes.shadow && attributes.shadow.color;
|
||||
|
|
@ -274,21 +287,21 @@ async function getAllChildElementsAttributes({ element, rootRect = null, depth =
|
|||
const isRootPosition = attributes.position &&
|
||||
attributes.position.left === 0 &&
|
||||
attributes.position.top === 0 &&
|
||||
attributes.position.width === currentRootRect.width &&
|
||||
attributes.position.height === currentRootRect.height;
|
||||
attributes.position.width === rootRect!.width &&
|
||||
attributes.position.height === rootRect!.height;
|
||||
|
||||
const hasOtherProperties = hasBackground || hasBorder || hasShadow || hasText || hasImage || isSvg || isCanvas || isTable;
|
||||
return hasOtherProperties && !isRootPosition;
|
||||
}) : allResults;
|
||||
|
||||
if (!rootRect) {
|
||||
if (depth === 0) {
|
||||
const sortedElements = filteredResults
|
||||
.sort((a, b) => {
|
||||
const zIndexA = a.attributes.zIndex || 0;
|
||||
const zIndexB = b.attributes.zIndex || 0;
|
||||
|
||||
if (zIndexA === zIndexB) {
|
||||
return b.depth - a.depth;
|
||||
return a.depth - b.depth;
|
||||
}
|
||||
|
||||
return zIndexB - zIndexA;
|
||||
|
|
@ -404,20 +417,39 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
|
|||
function parseBackground(computedStyles: CSSStyleDeclaration) {
|
||||
const backgroundColorResult = colorToHex(computedStyles.backgroundColor);
|
||||
|
||||
return {
|
||||
const background = {
|
||||
color: backgroundColorResult.hex,
|
||||
opacity: backgroundColorResult.opacity
|
||||
};
|
||||
|
||||
// Return undefined if background has no meaningful values
|
||||
if (!background.color && background.opacity === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return background;
|
||||
}
|
||||
|
||||
function parseBorder(computedStyles: CSSStyleDeclaration) {
|
||||
const borderColorResult = colorToHex(computedStyles.borderColor);
|
||||
const borderWidth = parseFloat(computedStyles.borderWidth);
|
||||
return borderWidth === 0 ? undefined : {
|
||||
|
||||
if (borderWidth === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const border = {
|
||||
color: borderColorResult.hex,
|
||||
width: isNaN(borderWidth) ? undefined : borderWidth,
|
||||
opacity: borderColorResult.opacity,
|
||||
};
|
||||
|
||||
// Return undefined if border has no meaningful values
|
||||
if (!border.color && border.width === undefined && border.opacity === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return border;
|
||||
}
|
||||
|
||||
function parseShadow(computedStyles: CSSStyleDeclaration) {
|
||||
|
|
@ -590,31 +622,32 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
|
|||
const blurRadius = numericParts.length >= 3 ? numericParts[2] : 0;
|
||||
const spreadRadius = numericParts.length >= 4 ? numericParts[3] : 0;
|
||||
|
||||
let shadowColor = 'rgba(0, 0, 0, 0.3)'; // default color
|
||||
// Only create shadow if color is present
|
||||
if (colorParts.length > 0) {
|
||||
shadowColor = colorParts.join(' ');
|
||||
}
|
||||
const shadowColor = colorParts.join(' ');
|
||||
const shadowColorResult = colorToHex(shadowColor);
|
||||
|
||||
const shadowColorResult = colorToHex(shadowColor);
|
||||
|
||||
const hasValidValues = offsetX !== 0 || offsetY !== 0 || blurRadius > 0 || spreadRadius !== 0 ||
|
||||
(shadowColorResult.hex && shadowColorResult.hex !== '000000' && shadowColorResult.opacity !== 0);
|
||||
|
||||
if (hasValidValues) {
|
||||
shadow = {
|
||||
offset: [offsetX, offsetY],
|
||||
color: shadowColorResult.hex || '000000',
|
||||
opacity: shadowColorResult.opacity,
|
||||
radius: blurRadius,
|
||||
spread: spreadRadius,
|
||||
inset: isInset,
|
||||
angle: Math.atan2(offsetY, offsetX) * (180 / Math.PI),
|
||||
};
|
||||
if (shadowColorResult.hex) {
|
||||
shadow = {
|
||||
offset: [offsetX, offsetY],
|
||||
color: shadowColorResult.hex,
|
||||
opacity: shadowColorResult.opacity,
|
||||
radius: blurRadius,
|
||||
spread: spreadRadius,
|
||||
inset: isInset,
|
||||
angle: Math.atan2(offsetY, offsetX) * (180 / Math.PI),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Return undefined if shadow is empty (no meaningful values)
|
||||
if (Object.keys(shadow).length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return shadow;
|
||||
}
|
||||
|
||||
|
|
@ -631,13 +664,20 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
|
|||
fontName = firstFont;
|
||||
}
|
||||
|
||||
return {
|
||||
const font = {
|
||||
name: fontName,
|
||||
size: isNaN(fontSize) ? undefined : fontSize,
|
||||
weight: isNaN(fontWeight) ? undefined : fontWeight,
|
||||
color: fontColorResult.hex,
|
||||
italic: fontStyle === 'italic',
|
||||
};
|
||||
|
||||
// Return undefined if font has no meaningful values
|
||||
if (!font.name && font.size === undefined && font.weight === undefined && !font.color && !font.italic) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return font;
|
||||
}
|
||||
|
||||
function parseLineHeight(computedStyles: CSSStyleDeclaration, el: Element) {
|
||||
|
|
@ -722,7 +762,7 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
|
|||
const rect = el.getBoundingClientRect();
|
||||
const maxRadiusX = rect.width / 2;
|
||||
const maxRadiusY = rect.height / 2;
|
||||
|
||||
|
||||
borderRadiusValue = borderRadiusValue.map((radius, index) => {
|
||||
// For top-left and bottom-right corners, use maxRadiusX
|
||||
// For top-right and bottom-left corners, use maxRadiusY
|
||||
|
|
@ -779,27 +819,29 @@ async function getElementAttributes(element: ElementHandle<Element>): Promise<El
|
|||
|
||||
return {
|
||||
tagName: el.tagName.toLowerCase(),
|
||||
id: el.id || undefined,
|
||||
id: el.id,
|
||||
className: (el.className && typeof el.className === 'string') ? el.className : (el.className ? el.className.toString() : undefined),
|
||||
innerText,
|
||||
background,
|
||||
border,
|
||||
shadow,
|
||||
font,
|
||||
position,
|
||||
margin,
|
||||
padding,
|
||||
innerText: innerText,
|
||||
background: background,
|
||||
border: border,
|
||||
shadow: shadow,
|
||||
font: font,
|
||||
position: position,
|
||||
margin: margin,
|
||||
padding: padding,
|
||||
zIndex: zIndexValue,
|
||||
textAlign: textAlign !== 'left' ? textAlign : undefined,
|
||||
lineHeight,
|
||||
lineHeight: lineHeight,
|
||||
borderRadius: borderRadiusValue,
|
||||
imageSrc: imageSrc || undefined,
|
||||
objectFit,
|
||||
imageSrc: imageSrc,
|
||||
objectFit: objectFit,
|
||||
clip: false,
|
||||
overlay: undefined,
|
||||
shape,
|
||||
shape: shape,
|
||||
connectorType: undefined,
|
||||
textWrap,
|
||||
textWrap: textWrap,
|
||||
should_screenshot: false,
|
||||
element: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -20,9 +20,6 @@ import {
|
|||
PptxConnectorType
|
||||
} from "@/types/pptx_models";
|
||||
|
||||
/**
|
||||
* Converts text alignment string to PptxAlignment enum value
|
||||
*/
|
||||
function convertTextAlignToPptxAlignment(textAlign?: string): PptxAlignment | undefined {
|
||||
if (!textAlign) return undefined;
|
||||
|
||||
|
|
@ -40,45 +37,33 @@ function convertTextAlignToPptxAlignment(textAlign?: string): PptxAlignment | un
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts line height from pixels to relative format (e.g., 1.5)
|
||||
* If lineHeight is already a relative number (less than 10), return as is
|
||||
* Otherwise, convert from pixels to relative by dividing by font size
|
||||
*/
|
||||
function convertLineHeightToRelative(lineHeight?: number, fontSize?: number): number | undefined {
|
||||
if (!lineHeight) return undefined;
|
||||
|
||||
let calculatedLineHeight = 1.2;
|
||||
// If lineHeight is already a relative number (typically between 1.0 and 3.0)
|
||||
if (lineHeight < 10) {
|
||||
calculatedLineHeight = lineHeight;
|
||||
}
|
||||
|
||||
// If we have font size, convert from pixels to relative
|
||||
if (fontSize && fontSize > 0) {
|
||||
calculatedLineHeight = Math.round((lineHeight / fontSize) * 100) / 100; // Round to 2 decimal places
|
||||
calculatedLineHeight = Math.round((lineHeight / fontSize) * 100) / 100;
|
||||
}
|
||||
|
||||
return calculatedLineHeight - 0.4 + (fontSize ?? 16) * 0.004;
|
||||
return calculatedLineHeight - 0.3
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts SlideAttributesResult[] to PptxSlideModel[]
|
||||
* Each SlideAttributesResult represents elements on a slide
|
||||
*/
|
||||
export function convertElementAttributesToPptxSlides(
|
||||
slidesAttributes: SlideAttributesResult[]
|
||||
): PptxSlideModel[] {
|
||||
return slidesAttributes.map((slideAttributes) => {
|
||||
const shapes = slideAttributes.elements.map(element => {
|
||||
return convertElementToPptxShape(element);
|
||||
}).filter(Boolean); // Remove any null/undefined shapes
|
||||
}).filter(Boolean);
|
||||
|
||||
const slide: PptxSlideModel = {
|
||||
shapes: shapes as (PptxTextBoxModel | PptxAutoShapeBoxModel | PptxConnectorModel | PptxPictureBoxModel)[]
|
||||
};
|
||||
|
||||
// Add background color if available
|
||||
if (slideAttributes.backgroundColor) {
|
||||
slide.background = {
|
||||
color: slideAttributes.backgroundColor,
|
||||
|
|
@ -90,39 +75,28 @@ export function convertElementAttributesToPptxSlides(
|
|||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a single ElementAttributes to the appropriate PPTX shape model
|
||||
*/
|
||||
function convertElementToPptxShape(
|
||||
element: ElementAttributes
|
||||
): PptxTextBoxModel | PptxAutoShapeBoxModel | PptxConnectorModel | PptxPictureBoxModel | null {
|
||||
// Skip elements without position
|
||||
if (!element.position) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if it's an image element
|
||||
if (element.tagName === 'img' || (element.className && typeof element.className === 'string' && element.className.includes('image')) || element.imageSrc) {
|
||||
return convertToPictureBox(element);
|
||||
}
|
||||
|
||||
// Check if it's a text element
|
||||
if (element.innerText && element.innerText.trim().length > 0) {
|
||||
return convertToTextBox(element);
|
||||
}
|
||||
|
||||
// Check if it's a connector/line element
|
||||
if (element.tagName === 'hr' || (element.className && typeof element.className === 'string' && (element.className.includes('connector') || element.className.includes('line')))) {
|
||||
return convertToConnector(element);
|
||||
}
|
||||
|
||||
// Default to auto shape box for other elements
|
||||
return convertToAutoShapeBox(element);
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts element to PptxTextBoxModel
|
||||
*/
|
||||
function convertToTextBox(element: ElementAttributes): PptxTextBoxModel {
|
||||
const position: PptxPositionModel = {
|
||||
left: Math.round(element.position?.left ?? 0),
|
||||
|
|
@ -161,9 +135,6 @@ function convertToTextBox(element: ElementAttributes): PptxTextBoxModel {
|
|||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts element to PptxAutoShapeBoxModel
|
||||
*/
|
||||
function convertToAutoShapeBox(element: ElementAttributes): PptxAutoShapeBoxModel {
|
||||
const position: PptxPositionModel = {
|
||||
left: Math.round(element.position?.left ?? 0),
|
||||
|
|
@ -178,25 +149,24 @@ function convertToAutoShapeBox(element: ElementAttributes): PptxAutoShapeBoxMode
|
|||
|
||||
const stroke: PptxStrokeModel | undefined = element.border?.color ? {
|
||||
color: element.border.color,
|
||||
thickness: element.border.width ?? 1, // float - keep as number
|
||||
thickness: element.border.width ?? 1,
|
||||
opacity: element.border.opacity ?? 1.0
|
||||
} : undefined;
|
||||
|
||||
const shadow: PptxShadowModel | undefined = element.shadow?.color ? {
|
||||
radius: Math.round(element.shadow.radius ?? 4), // int
|
||||
offset: Math.round(element.shadow.offset ? Math.sqrt(element.shadow.offset[0] ** 2 + element.shadow.offset[1] ** 2) : 0), // int
|
||||
radius: Math.round(element.shadow.radius ?? 4),
|
||||
offset: Math.round(element.shadow.offset ? Math.sqrt(element.shadow.offset[0] ** 2 + element.shadow.offset[1] ** 2) : 0),
|
||||
color: element.shadow.color,
|
||||
opacity: element.shadow.opacity ?? 0.5, // float - keep as number
|
||||
angle: Math.round(element.shadow.angle ?? 0) // int
|
||||
opacity: element.shadow.opacity ?? 0.5,
|
||||
angle: Math.round(element.shadow.angle ?? 0)
|
||||
} : undefined;
|
||||
|
||||
// Check if element has text content
|
||||
const paragraphs: PptxParagraphModel[] | undefined = element.innerText ? [{
|
||||
spacing: undefined,
|
||||
alignment: convertTextAlignToPptxAlignment(element.textAlign),
|
||||
font: element.font ? {
|
||||
name: element.font.name ?? "Inter",
|
||||
size: Math.round(element.font.size ?? 16), // int
|
||||
size: Math.round(element.font.size ?? 16),
|
||||
font_weight: element.font.weight ?? 400,
|
||||
italic: element.font.italic ?? false,
|
||||
color: element.font.color ?? "000000"
|
||||
|
|
@ -205,22 +175,21 @@ function convertToAutoShapeBox(element: ElementAttributes): PptxAutoShapeBoxMode
|
|||
text: element.innerText
|
||||
}] : undefined;
|
||||
|
||||
const shapeType = element.borderRadius ? PptxShapeType.ROUNDED_RECTANGLE : PptxShapeType.RECTANGLE;
|
||||
|
||||
return {
|
||||
type: PptxShapeType.ROUNDED_RECTANGLE, // Default to rounded rectangle
|
||||
type: shapeType,
|
||||
margin: undefined,
|
||||
fill,
|
||||
stroke,
|
||||
shadow,
|
||||
position,
|
||||
text_wrap: element.textWrap ?? true,
|
||||
border_radius: element.borderRadius ? Math.round(element.borderRadius[0]) : 0, // int - use first value for autoshape
|
||||
border_radius: element.borderRadius ? Math.round(element.borderRadius[0]) : undefined,
|
||||
paragraphs
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts element to PptxPictureBoxModel
|
||||
*/
|
||||
function convertToPictureBox(element: ElementAttributes): PptxPictureBoxModel {
|
||||
const position: PptxPositionModel = {
|
||||
left: Math.round(element.position?.left ?? 0),
|
||||
|
|
@ -233,8 +202,6 @@ function convertToPictureBox(element: ElementAttributes): PptxPictureBoxModel {
|
|||
fit: element.objectFit ? (element.objectFit as PptxObjectFitEnum) : PptxObjectFitEnum.CONTAIN
|
||||
};
|
||||
|
||||
|
||||
// Extract image path from element attributes
|
||||
const picture: PptxPictureModel = {
|
||||
is_network: element.imageSrc ? element.imageSrc.startsWith('http') : false,
|
||||
path: element.imageSrc || ''
|
||||
|
|
@ -245,16 +212,13 @@ function convertToPictureBox(element: ElementAttributes): PptxPictureBoxModel {
|
|||
margin: undefined,
|
||||
clip: element.clip ?? true,
|
||||
overlay: element.overlay,
|
||||
border_radius: element.borderRadius ? element.borderRadius.map(r => Math.round(r)) : undefined, // List[int] - 4 elements from route parsing
|
||||
border_radius: element.borderRadius ? element.borderRadius.map(r => Math.round(r)) : undefined,
|
||||
shape: element.shape ? (element.shape as PptxBoxShapeEnum) : PptxBoxShapeEnum.RECTANGLE,
|
||||
object_fit: objectFit,
|
||||
picture
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts element to PptxConnectorModel
|
||||
*/
|
||||
function convertToConnector(element: ElementAttributes): PptxConnectorModel {
|
||||
const position: PptxPositionModel = {
|
||||
left: Math.round(element.position?.left ?? 0),
|
||||
|
|
@ -264,9 +228,9 @@ function convertToConnector(element: ElementAttributes): PptxConnectorModel {
|
|||
};
|
||||
|
||||
return {
|
||||
type: PptxConnectorType.STRAIGHT, // Default to straight connector
|
||||
type: PptxConnectorType.STRAIGHT,
|
||||
position,
|
||||
thickness: element.border?.width ?? 0.5, // float - keep as number
|
||||
thickness: element.border?.width ?? 0.5,
|
||||
color: element.border?.color || element.background?.color || '000000',
|
||||
opacity: element.border?.opacity ?? 1.0
|
||||
};
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue