273 lines
8.8 KiB
TypeScript
273 lines
8.8 KiB
TypeScript
import { ElementAttributes, SlideAttributesResult } from "@/types/element_attibutes";
|
|
import {
|
|
PptxSlideModel,
|
|
PptxTextBoxModel,
|
|
PptxAutoShapeBoxModel,
|
|
PptxPictureBoxModel,
|
|
PptxConnectorModel,
|
|
PptxPositionModel,
|
|
PptxFillModel,
|
|
PptxStrokeModel,
|
|
PptxShadowModel,
|
|
PptxFontModel,
|
|
PptxParagraphModel,
|
|
PptxPictureModel,
|
|
PptxObjectFitModel,
|
|
PptxBoxShapeEnum,
|
|
PptxObjectFitEnum,
|
|
PptxAlignment,
|
|
PptxShapeType,
|
|
PptxConnectorType
|
|
} from "@/types/pptx_models";
|
|
|
|
/**
|
|
* Converts text alignment string to PptxAlignment enum value
|
|
*/
|
|
function convertTextAlignToPptxAlignment(textAlign?: string): PptxAlignment | undefined {
|
|
if (!textAlign) return undefined;
|
|
|
|
switch (textAlign.toLowerCase()) {
|
|
case 'left':
|
|
return PptxAlignment.LEFT;
|
|
case 'center':
|
|
return PptxAlignment.CENTER;
|
|
case 'right':
|
|
return PptxAlignment.RIGHT;
|
|
case 'justify':
|
|
return PptxAlignment.JUSTIFY;
|
|
default:
|
|
return PptxAlignment.LEFT;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
}
|
|
|
|
return calculatedLineHeight - 0.4 + (fontSize ?? 16) * 0.004;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
|
|
const slide: PptxSlideModel = {
|
|
shapes: shapes as (PptxTextBoxModel | PptxAutoShapeBoxModel | PptxConnectorModel | PptxPictureBoxModel)[]
|
|
};
|
|
|
|
// Add background color if available
|
|
if (slideAttributes.backgroundColor) {
|
|
slide.background = {
|
|
color: slideAttributes.backgroundColor,
|
|
opacity: 1.0
|
|
};
|
|
}
|
|
|
|
return slide;
|
|
});
|
|
}
|
|
|
|
/**
|
|
* 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),
|
|
top: Math.round(element.position?.top ?? 0),
|
|
width: Math.round(element.position?.width ?? 0),
|
|
height: Math.round(element.position?.height ?? 0)
|
|
};
|
|
|
|
const fill: PptxFillModel | undefined = element.background?.color ? {
|
|
color: element.background.color,
|
|
opacity: element.background.opacity ?? 1.0
|
|
} : undefined;
|
|
|
|
const font: PptxFontModel | undefined = element.font ? {
|
|
name: element.font.name ?? "Inter",
|
|
size: Math.round(element.font.size ?? 16),
|
|
font_weight: element.font.weight ?? 400,
|
|
italic: element.font.italic ?? false,
|
|
color: element.font.color ?? "000000"
|
|
} : undefined;
|
|
|
|
const paragraph: PptxParagraphModel = {
|
|
spacing: undefined,
|
|
alignment: convertTextAlignToPptxAlignment(element.textAlign),
|
|
font,
|
|
line_height: convertLineHeightToRelative(element.lineHeight, element.font?.size),
|
|
text: element.innerText
|
|
};
|
|
|
|
return {
|
|
margin: undefined,
|
|
fill,
|
|
position,
|
|
text_wrap: element.textWrap ?? true,
|
|
paragraphs: [paragraph]
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Converts element to PptxAutoShapeBoxModel
|
|
*/
|
|
function convertToAutoShapeBox(element: ElementAttributes): PptxAutoShapeBoxModel {
|
|
const position: PptxPositionModel = {
|
|
left: Math.round(element.position?.left ?? 0),
|
|
top: Math.round(element.position?.top ?? 0),
|
|
width: Math.round(element.position?.width ?? 0),
|
|
height: Math.round(element.position?.height ?? 0)
|
|
};
|
|
const fill: PptxFillModel | undefined = element.background?.color ? {
|
|
color: element.background.color,
|
|
opacity: element.background.opacity ?? 1.0
|
|
} : undefined;
|
|
|
|
const stroke: PptxStrokeModel | undefined = element.border?.color ? {
|
|
color: element.border.color,
|
|
thickness: element.border.width ?? 1, // float - keep as number
|
|
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
|
|
color: element.shadow.color,
|
|
opacity: element.shadow.opacity ?? 0.5, // float - keep as number
|
|
angle: Math.round(element.shadow.angle ?? 0) // int
|
|
} : 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
|
|
font_weight: element.font.weight ?? 400,
|
|
italic: element.font.italic ?? false,
|
|
color: element.font.color ?? "000000"
|
|
} : undefined,
|
|
line_height: convertLineHeightToRelative(element.lineHeight, element.font?.size),
|
|
text: element.innerText
|
|
}] : undefined;
|
|
|
|
return {
|
|
type: PptxShapeType.ROUNDED_RECTANGLE, // Default to rounded rectangle
|
|
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
|
|
paragraphs
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Converts element to PptxPictureBoxModel
|
|
*/
|
|
function convertToPictureBox(element: ElementAttributes): PptxPictureBoxModel {
|
|
const position: PptxPositionModel = {
|
|
left: Math.round(element.position?.left ?? 0),
|
|
top: Math.round(element.position?.top ?? 0),
|
|
width: Math.round(element.position?.width ?? 0),
|
|
height: Math.round(element.position?.height ?? 0)
|
|
};
|
|
|
|
const objectFit: PptxObjectFitModel = {
|
|
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 || ''
|
|
};
|
|
|
|
return {
|
|
position,
|
|
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
|
|
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),
|
|
top: Math.round(element.position?.top ?? 0),
|
|
width: Math.round(element.position?.width ?? 0),
|
|
height: Math.round(element.position?.height ?? 0)
|
|
};
|
|
|
|
return {
|
|
type: PptxConnectorType.STRAIGHT, // Default to straight connector
|
|
position,
|
|
thickness: element.border?.width ?? 0.5, // float - keep as number
|
|
color: element.border?.color || element.background?.color || '000000',
|
|
opacity: element.border?.opacity ?? 1.0
|
|
};
|
|
}
|