mirror of
https://github.com/CherryHQ/cherry-studio.git
synced 2025-12-27 21:01:32 +08:00
fix(renderer): Optimize full-content export for complex layouts
This commit addresses performance and reliability issues with full-content export in side-by-side and grid layouts by implementing targeted improvements: - Replace canvasWidth/canvasHeight with width/height in html-to-image options for more accurate style-based sizing of exported content - Optimize scrollable container expansion by only modifying actually clipped elements instead of all scrollables, reducing unnecessary DOM manipulations - Improve style management using Map for aggregated inline style storage and reverse-order restoration to maintain proper CSS cascading behavior - Prevent layout breakage in horizontal scroll regions by using minWidth instead of width to preserve content flow during export process - Enhance rendering stability with requestAnimationFrame to wait for layout settlement and pixelRatio capping to avoid exceeding browser canvas limitations These changes improve export quality, reduce performance overhead, and prevent common issues like clipped content, layout distortion, and canvas size errors in complex multi-column layouts.
This commit is contained in:
parent
e92267f2b7
commit
622901178c
@ -72,8 +72,26 @@ export const captureScrollable = async (elRef: React.RefObject<HTMLElement | nul
|
||||
const originalScrollTop = el.scrollTop
|
||||
|
||||
// Track nested elements we temporarily modify
|
||||
type SavedStyle = Partial<Pick<CSSStyleDeclaration, 'overflow' | 'overflowX' | 'overflowY' | 'height' | 'maxHeight' | 'width'>>
|
||||
const modifiedElements: Array<{ node: HTMLElement; style: SavedStyle }> = []
|
||||
type SavedStyle = Partial<
|
||||
Pick<CSSStyleDeclaration, 'overflow' | 'overflowX' | 'overflowY' | 'height' | 'maxHeight' | 'width' | 'minWidth'>
|
||||
>
|
||||
const savedStyles = new Map<HTMLElement, SavedStyle>()
|
||||
const modifiedNodes: HTMLElement[] = []
|
||||
const save = (node: HTMLElement, fields: Array<keyof SavedStyle>) => {
|
||||
let saved = savedStyles.get(node)
|
||||
if (!saved) {
|
||||
saved = {}
|
||||
savedStyles.set(node, saved)
|
||||
modifiedNodes.push(node)
|
||||
}
|
||||
for (const f of fields) {
|
||||
if (saved[f] === undefined) {
|
||||
// Record inline style only once
|
||||
// @ts-expect-error index signature
|
||||
saved[f] = node.style[f]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Hide scrollbars during capture
|
||||
el.classList.add('hide-scrollbar')
|
||||
@ -89,34 +107,39 @@ export const captureScrollable = async (elRef: React.RefObject<HTMLElement | nul
|
||||
const all = Array.from(el.querySelectorAll<HTMLElement>('*'))
|
||||
for (const node of all) {
|
||||
const cs = getComputedStyle(node)
|
||||
const save = (fields: Array<keyof SavedStyle>) => {
|
||||
const saved: SavedStyle = {}
|
||||
for (const f of fields) saved[f] = (node.style as any)[f]
|
||||
modifiedElements.push({ node, style: saved })
|
||||
}
|
||||
if (cs.display === 'none') continue
|
||||
|
||||
// Expand vertical scroll regions
|
||||
if (cs.overflowY === 'auto' || cs.overflowY === 'scroll' || cs.overflowY === 'hidden') {
|
||||
save(['overflow', 'overflowY', 'height', 'maxHeight'])
|
||||
// Expand vertical scroll regions only if content is clipped
|
||||
const needsV =
|
||||
(cs.overflowY === 'auto' || cs.overflowY === 'scroll' || cs.overflowY === 'hidden') &&
|
||||
node.scrollHeight > node.clientHeight
|
||||
if (needsV) {
|
||||
save(node, ['overflow', 'overflowY', 'height', 'maxHeight'])
|
||||
node.style.overflowY = 'visible'
|
||||
node.style.overflow = 'visible'
|
||||
node.style.maxHeight = 'none'
|
||||
node.style.height = 'auto'
|
||||
}
|
||||
|
||||
// Expand horizontal scroll regions
|
||||
if (cs.overflowX === 'auto' || cs.overflowX === 'scroll' || cs.overflowX === 'hidden') {
|
||||
save(['overflow', 'overflowX', 'width'])
|
||||
// Expand horizontal scroll regions only if content is clipped
|
||||
const needsH =
|
||||
(cs.overflowX === 'auto' || cs.overflowX === 'scroll' || cs.overflowX === 'hidden') &&
|
||||
node.scrollWidth > node.clientWidth
|
||||
if (needsH) {
|
||||
save(node, ['overflow', 'overflowX', 'minWidth'])
|
||||
node.style.overflowX = 'visible'
|
||||
node.style.overflow = 'visible'
|
||||
// Ensure full width for horizontally-scrolling containers
|
||||
// Ensure full width for horizontally-scrolling containers using minWidth to avoid layout breakage
|
||||
const sw = Math.max(node.scrollWidth, node.clientWidth)
|
||||
if (sw > 0) node.style.width = `${sw}px`
|
||||
if (sw > 0) node.style.minWidth = `${sw}px`
|
||||
}
|
||||
|
||||
// Special case: message content containers in horizontal layout
|
||||
if (node.classList.contains('message-content-container')) {
|
||||
save(['maxHeight', 'overflow', 'overflowY', 'height'])
|
||||
if (
|
||||
node.classList.contains('message-content-container') &&
|
||||
node.scrollHeight > node.clientHeight
|
||||
) {
|
||||
save(node, ['maxHeight', 'overflow', 'overflowY', 'height'])
|
||||
node.style.maxHeight = 'none'
|
||||
node.style.overflow = 'visible'
|
||||
node.style.overflowY = 'visible'
|
||||
@ -127,7 +150,10 @@ export const captureScrollable = async (elRef: React.RefObject<HTMLElement | nul
|
||||
|
||||
expandNestedScrollables()
|
||||
|
||||
// calculate the size of the element
|
||||
// Wait one frame for layout to settle after style changes
|
||||
await new Promise((r) => requestAnimationFrame(() => r(null)))
|
||||
|
||||
// calculate the size of the element after expansion
|
||||
const totalWidth = el.scrollWidth
|
||||
const totalHeight = el.scrollHeight
|
||||
|
||||
@ -141,13 +167,17 @@ export const captureScrollable = async (elRef: React.RefObject<HTMLElement | nul
|
||||
el.style.position = originalStyle.position
|
||||
|
||||
// restore nested modified elements
|
||||
for (const { node, style } of modifiedElements.reverse()) {
|
||||
for (let i = modifiedNodes.length - 1; i >= 0; i--) {
|
||||
const node = modifiedNodes[i]
|
||||
const style = savedStyles.get(node)
|
||||
if (!style) continue
|
||||
if (style.overflow !== undefined) node.style.overflow = style.overflow
|
||||
if (style.overflowX !== undefined) node.style.overflowX = style.overflowX
|
||||
if (style.overflowY !== undefined) node.style.overflowY = style.overflowY
|
||||
if (style.height !== undefined) node.style.height = style.height
|
||||
if (style.maxHeight !== undefined) node.style.maxHeight = style.maxHeight
|
||||
if (style.width !== undefined) node.style.width = style.width
|
||||
if (style.minWidth !== undefined) node.style.minWidth = style.minWidth
|
||||
}
|
||||
|
||||
// restore the original scroll position
|
||||
@ -158,19 +188,26 @@ export const captureScrollable = async (elRef: React.RefObject<HTMLElement | nul
|
||||
window.toast.error(i18n.t('message.error.dimension_too_large'))
|
||||
return Promise.reject()
|
||||
}
|
||||
// Cap pixelRatio to avoid exceeding browser canvas limits
|
||||
const maxLogical = Math.max(totalWidth, totalHeight)
|
||||
const maxAllowedPR = Math.max(0.1, Math.min(window.devicePixelRatio, MAX_ALLOWED_DIMENSION / Math.max(1, maxLogical)))
|
||||
|
||||
const canvas = await new Promise<HTMLCanvasElement>((resolve, reject) => {
|
||||
htmlToImage
|
||||
.toCanvas(el, {
|
||||
backgroundColor: getComputedStyle(el).getPropertyValue('--color-background'),
|
||||
cacheBust: true,
|
||||
pixelRatio: window.devicePixelRatio,
|
||||
pixelRatio: maxAllowedPR,
|
||||
skipAutoScale: true,
|
||||
width: el.scrollWidth,
|
||||
height: el.scrollHeight,
|
||||
width: totalWidth,
|
||||
height: totalHeight,
|
||||
style: {
|
||||
backgroundColor: getComputedStyle(el).backgroundColor,
|
||||
color: getComputedStyle(el).color
|
||||
color: getComputedStyle(el).color,
|
||||
width: `${totalWidth}px`,
|
||||
height: `${totalHeight}px`,
|
||||
overflow: 'visible',
|
||||
display: 'block'
|
||||
}
|
||||
})
|
||||
.then((canvas) => resolve(canvas))
|
||||
@ -184,13 +221,17 @@ export const captureScrollable = async (elRef: React.RefObject<HTMLElement | nul
|
||||
el.style.position = originalStyle.position
|
||||
|
||||
// Restore nested modified elements
|
||||
for (const { node, style } of modifiedElements.reverse()) {
|
||||
for (let i = modifiedNodes.length - 1; i >= 0; i--) {
|
||||
const node = modifiedNodes[i]
|
||||
const style = savedStyles.get(node)
|
||||
if (!style) continue
|
||||
if (style.overflow !== undefined) node.style.overflow = style.overflow
|
||||
if (style.overflowX !== undefined) node.style.overflowX = style.overflowX
|
||||
if (style.overflowY !== undefined) node.style.overflowY = style.overflowY
|
||||
if (style.height !== undefined) node.style.height = style.height
|
||||
if (style.maxHeight !== undefined) node.style.maxHeight = style.maxHeight
|
||||
if (style.width !== undefined) node.style.width = style.width
|
||||
if (style.minWidth !== undefined) node.style.minWidth = style.minWidth
|
||||
}
|
||||
|
||||
const imageData = canvas
|
||||
|
||||
Loading…
Reference in New Issue
Block a user