Skip to content

Commit 8e30176

Browse files
Bill LeoutsakosBill Leoutsakos
authored andcommitted
fix(files): preserve PDF image links and table alignment
1 parent 46ecbf4 commit 8e30176

2 files changed

Lines changed: 108 additions & 13 deletions

File tree

apps/sim/app/api/files/export/[id]/markdown-pdf.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,6 +150,61 @@ ${repeatedParagraphs}`
150150
expect(text).toContain('Value')
151151
})
152152

153+
it('preserves links on linked-image fallbacks', async () => {
154+
const destination = 'https://sim.ai/docs'
155+
const buffer = await renderMarkdownPdf({
156+
markdown: `[![Documentation badge](https://example.com/badge.svg)](${destination})`,
157+
title: 'Linked image',
158+
})
159+
160+
const document = await getDocument({ data: new Uint8Array(buffer), disableWorker: true })
161+
.promise
162+
try {
163+
const annotations = await (await document.getPage(1)).getAnnotations()
164+
expect(annotations.some((annotation) => annotation.url === destination)).toBe(true)
165+
} finally {
166+
await document.destroy()
167+
}
168+
})
169+
170+
it('preserves GFM table-cell alignment', async () => {
171+
const buffer = await renderMarkdownPdf({
172+
markdown: `| LEFTVALUE |
173+
| :--- |
174+
| left |
175+
176+
| CENTERVALUE |
177+
| :---: |
178+
| center |
179+
180+
| RIGHTVALUE |
181+
| ---: |
182+
| right |`,
183+
title: 'Aligned tables',
184+
})
185+
186+
const document = await getDocument({ data: new Uint8Array(buffer), disableWorker: true })
187+
.promise
188+
try {
189+
const content = await (await document.getPage(1)).getTextContent()
190+
const textX = (value: string) => {
191+
const item = content.items.find(
192+
(candidate) => 'str' in candidate && candidate.str === value
193+
)
194+
expect(item && 'transform' in item).toBe(true)
195+
return item && 'transform' in item ? item.transform[4] : 0
196+
}
197+
const leftX = textX('LEFTVALUE')
198+
const centerX = textX('CENTERVALUE')
199+
const rightX = textX('RIGHTVALUE')
200+
201+
expect(centerX - leftX).toBeGreaterThan(100)
202+
expect(rightX - centerX).toBeGreaterThan(100)
203+
} finally {
204+
await document.destroy()
205+
}
206+
})
207+
153208
it('falls back instead of decoding an image above the pixel ceiling', async () => {
154209
const oversizedSvg = Buffer.from(
155210
'<svg xmlns="http://www.w3.org/2000/svg" width="20000" height="20000"><rect width="100%" height="100%" fill="red"/></svg>'

apps/sim/app/api/files/export/[id]/markdown-pdf.tsx

Lines changed: 53 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,8 @@ const styles = StyleSheet.create({
249249
minWidth: 0,
250250
padding: 5,
251251
},
252+
tableCellCenter: { textAlign: 'center' },
253+
tableCellRight: { textAlign: 'right' },
252254
tableHeader: { backgroundColor: '#f1f3f5', fontWeight: 700 },
253255
imageBlock: { marginBottom: 11 },
254256
image: { maxHeight: 430, objectFit: 'contain', width: '100%' },
@@ -451,10 +453,24 @@ function renderImage(
451453
key: string
452454
): ReactNode {
453455
const src = stringAttr(node, 'src') ?? ''
456+
const rawHref = stringAttr(node, 'href')
457+
const href = rawHref ? safeLink(rawHref) : undefined
454458
const ref = extractEmbeddedFileRef(src)
455459
const image = ref ? images.get(markdownPdfImageKey(ref)) : undefined
456460
if (!image) {
457461
const alt = stringAttr(node, 'alt')
462+
const fallback = (
463+
<Text style={styles.imageFallback}>
464+
{renderText(alt ? `Image: ${alt}` : 'Image unavailable', key)}
465+
</Text>
466+
)
467+
if (href) {
468+
return (
469+
<Link key={key} src={href}>
470+
{fallback}
471+
</Link>
472+
)
473+
}
458474
return (
459475
<Text key={key} style={styles.imageFallback}>
460476
{renderText(alt ? `Image: ${alt}` : 'Image unavailable', key)}
@@ -466,6 +482,18 @@ function renderImage(
466482
const width = Number.isFinite(requestedWidth)
467483
? Math.min(Math.max(requestedWidth, 1), PDF_TABLE_CONTENT_WIDTH)
468484
: undefined
485+
const renderedImage = (
486+
<View style={styles.imageBlock}>
487+
<Image src={image} style={[styles.image, ...(width ? [{ width }] : [])]} />
488+
</View>
489+
)
490+
if (href) {
491+
return (
492+
<Link key={key} src={href}>
493+
{renderedImage}
494+
</Link>
495+
)
496+
}
469497
return (
470498
<View key={key} style={styles.imageBlock}>
471499
<Image src={image} style={[styles.image, ...(width ? [{ width }] : [])]} />
@@ -515,19 +543,31 @@ function renderList(
515543
function renderTableRow(row: JSONContent, key: string, header: boolean): ReactNode {
516544
return (
517545
<View key={key} minPresenceAhead={header ? 16 : undefined} style={styles.tableRow}>
518-
{(row.content ?? []).map((cell, index) => (
519-
<Text
520-
key={`${key}-${index}`}
521-
style={[styles.tableCell, ...(header ? [styles.tableHeader] : [])]}
522-
>
523-
{(cell.content ?? []).map((child, childIndex) => (
524-
<Text key={`${key}-${index}-${childIndex}`}>
525-
{childIndex > 0 ? '\n' : null}
526-
{renderInline(`${key}-${index}-${childIndex}`, child.content)}
527-
</Text>
528-
))}
529-
</Text>
530-
))}
546+
{(row.content ?? []).map((cell, index) => {
547+
const alignmentStyle =
548+
cell.attrs?.align === 'center'
549+
? styles.tableCellCenter
550+
: cell.attrs?.align === 'right'
551+
? styles.tableCellRight
552+
: undefined
553+
return (
554+
<Text
555+
key={`${key}-${index}`}
556+
style={[
557+
styles.tableCell,
558+
...(header ? [styles.tableHeader] : []),
559+
...(alignmentStyle ? [alignmentStyle] : []),
560+
]}
561+
>
562+
{(cell.content ?? []).map((child, childIndex) => (
563+
<Text key={`${key}-${index}-${childIndex}`}>
564+
{childIndex > 0 ? '\n' : null}
565+
{renderInline(`${key}-${index}-${childIndex}`, child.content)}
566+
</Text>
567+
))}
568+
</Text>
569+
)
570+
})}
531571
</View>
532572
)
533573
}

0 commit comments

Comments
 (0)