You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

155 lines
4.4 KiB

/**
* Document Renderer
*
* High-level document rendering that combines layout calculation with PDF rendering.
*/
import type { Node, LayoutBox, DocumentNode } from '../core/model/types'
import { isDocumentNode, hasChildren, isTextNode, isImageNode, isDividerNode, isBoxNode } from '../core/model/types'
import { getPageDimensions, normalizeSpacing } from '../core/model/node'
import { calculateLayout } from '../core/layout'
import type { Renderer } from './types'
import { nodeStyleToTextStyle, nodeStyleToRectStyle } from './types'
/**
* Options for document rendering
*/
export interface DocumentRenderOptions {
/** Renderer instance to use */
renderer: Renderer
}
/**
* Render a document tree to PDF
*/
export async function renderDocument(root: Node, options: DocumentRenderOptions): Promise<Renderer> {
const { renderer } = options
// Validate root is a document node
if (!isDocumentNode(root)) {
throw new Error('Root node must be a document node')
}
// Get page dimensions
const pageSettings = root.props.pageSettings
const { width: pageWidth, height: pageHeight } = getPageDimensions(
pageSettings.size,
pageSettings.orientation
)
const margins = normalizeSpacing(pageSettings.margins ?? [40, 40, 40, 40])
// Begin document
renderer.beginDocument({
pageWidth,
pageHeight,
metadata: root.props.metadata,
})
// Calculate layout for each child
const contentWidth = pageWidth - margins.left - margins.right
const contentHeight = pageHeight - margins.top - margins.bottom
let currentPage = 1
let currentY = margins.top
let remainingHeight = contentHeight
renderer.beginPage(currentPage)
// Render children
const children = root.children ?? []
for (const child of children) {
const layoutBox = calculateLayout(child, contentWidth, remainingHeight)
// Check if content fits on current page
if (layoutBox.height > remainingHeight && currentY > margins.top) {
// Need new page
currentPage++
renderer.endPage()
renderer.beginPage(currentPage)
currentY = margins.top
remainingHeight = contentHeight
}
// Offset the layout box to correct position
const offsetBox: LayoutBox = {
...layoutBox,
x: margins.left + layoutBox.x,
y: currentY + layoutBox.y,
}
// Render the node
await renderNode(child, offsetBox, renderer)
// Update position
currentY += layoutBox.height
remainingHeight -= layoutBox.height
}
renderer.endPage()
renderer.endDocument()
return renderer
}
/**
* Render a single node and its children
*/
async function renderNode(node: Node, box: LayoutBox, renderer: Renderer): Promise<void> {
// Render background and border for containers
if (hasChildren(node)) {
const rectStyle = nodeStyleToRectStyle(node.style)
if (rectStyle.backgroundColor || rectStyle.borderColor) {
renderer.drawRect(box, rectStyle)
}
}
// Render based on node type
if (isTextNode(node)) {
const textStyle = nodeStyleToTextStyle(node.style)
renderer.drawText(node.props.value, box, textStyle)
} else if (isImageNode(node)) {
await renderer.drawImage(node.props.src, box)
} else if (isDividerNode(node)) {
const thickness = node.props?.thickness ?? 1
const color = node.style?.borderColor ?? '#000000'
const style = node.props?.style ?? 'solid'
if (node.props?.orientation === 'vertical') {
renderer.drawLine(
{ x: box.x + box.width / 2, y: box.y },
{ x: box.x + box.width / 2, y: box.y + box.height },
{ color, thickness, style }
)
} else {
renderer.drawLine(
{ x: box.x, y: box.y + thickness / 2 },
{ x: box.x + box.width, y: box.y + thickness / 2 },
{ color, thickness, style }
)
}
} else if (hasChildren(node) && box.children) {
// Render children
for (let i = 0; i < node.children!.length; i++) {
const child = node.children![i]
const childBox = box.children[i]
await renderNode(child, childBox, renderer)
}
}
}
/**
* Export document to PDF blob
*/
export async function exportToBlob(root: Node, renderer: Renderer): Promise<Blob> {
await renderDocument(root, { renderer })
return renderer.toBlob()
}
/**
* Export document to data URL
*/
export async function exportToDataURL(root: Node, renderer: Renderer): Promise<string> {
await renderDocument(root, { renderer })
return renderer.toDataURL()
}

Powered by TurnKey Linux.