import ExcelJS from 'exceljs'; import JSZip from 'jszip'; import { compatibleImagePositions, compatibleImages, convertWorkbookOutput, loadCompatibleWorkbook, } from './workbook-compatibility'; const PNG = Buffer.from( 'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAusB9Y9ZrYQAAAAASUVORK5CYII=', 'base64', ); async function standardWorkbook() { const workbook = new ExcelJS.Workbook(); const sheet = workbook.addWorksheet('签名报备'); sheet.getCell('A1').value = '营业执照'; sheet.getCell('A2').value = 'license.png'; const imageId = workbook.addImage({ buffer: PNG as never, extension: 'png' }); sheet.addImage(imageId, { tl: { col: 0, row: 1 }, br: { col: 0.9, row: 1.9 }, editAs: 'oneCell' } as never); return Buffer.from(await workbook.xlsx.writeBuffer()); } const BROKEN_ID = 'ID_8E811861F79046DDAB7EDBD3AEE9DE89'; const BROKEN_NODE = ``; async function wpsFixture() { return JSZip.loadAsync(await convertWorkbookOutput(await standardWorkbook(), 'wps_cell_image')); } async function addBrokenNode(zip: JSZip, referenced: boolean) { const xml = await zip.file('xl/cellimages.xml')!.async('string'); zip.file('xl/cellimages.xml', xml.replace('', `${BROKEN_NODE}`)); if (referenced) { const sheet = await zip.file('xl/worksheets/sheet1.xml')!.async('string'); zip.file( 'xl/worksheets/sheet1.xml', sheet.replace( '', `_xlfn.DISPIMG("${BROKEN_ID}",1)`, ), ); } } describe('WPS workbook compatibility', () => { describe.each([false, true])('includeImageData=%s', (includeImageData) => { it('ignores an unreferenced damaged node without stealing the next image or shifting a blank cell', async () => { const zip = await wpsFixture(); await addBrokenNode(zip, false); const sheet = await zip.file('xl/worksheets/sheet1.xml')!.async('string'); zip.file('xl/worksheets/sheet1.xml', sheet.replace(' { const zip = await wpsFixture(); await addBrokenNode(zip, true); await expect( loadCompatibleWorkbook(await zip.generateAsync({ type: 'nodebuffer' }), { includeImageData }), ).rejects.toThrow('工作表【签名报备】B3的WPS图片引用缺失或不唯一'); }); it('rejects duplicate image IDs even when the later node is damaged', async () => { const zip = await wpsFixture(); const xml = await zip.file('xl/cellimages.xml')!.async('string'); const id = xml.match(/name="(ID_[A-F0-9]{32})"/)![1]; const duplicate = BROKEN_NODE.replace(BROKEN_ID, id); zip.file('xl/cellimages.xml', xml.replace('', `${duplicate}`)); await expect( loadCompatibleWorkbook(await zip.generateAsync({ type: 'nodebuffer' }), { includeImageData }), ).rejects.toThrow('工作表【签名报备】A2的WPS图片ID重复'); }); it('reports a missing media file with the exact worksheet and cell', async () => { const zip = await wpsFixture(); for (const name of Object.keys(zip.files).filter((name) => name.startsWith('xl/media/'))) zip.remove(name); await expect( loadCompatibleWorkbook(await zip.generateAsync({ type: 'nodebuffer' }), { includeImageData }), ).rejects.toThrow('工作表【签名报备】A2的WPS图片文件缺失'); }); it.each(['missing', 'duplicate', 'external'])('rejects %s image relationships', async (kind) => { const zip = await wpsFixture(); const path = 'xl/_rels/cellimages.xml.rels'; const xml = await zip.file(path)!.async('string'); const relationship = xml.match(/]*\/>/)![0]; zip.file( path, kind === 'missing' ? xml.replace(relationship, '') : kind === 'duplicate' ? xml.replace(relationship, relationship + relationship) : xml.replace(relationship, relationship.replace('/>', ' TargetMode="External"/>')), ); await expect( loadCompatibleWorkbook(await zip.generateAsync({ type: 'nodebuffer' }), { includeImageData }), ).rejects.toThrow('工作表【签名报备】A2的WPS图片引用缺失或不唯一'); }); }); it('converts a standard Drawing image to DISPIMG and reads it back from cellimages.xml', async () => { const converted = await convertWorkbookOutput(await standardWorkbook(), 'wps_cell_image'); const zip = await JSZip.loadAsync(converted); await expect(zip.file('xl/cellimages.xml')!.async('string')).resolves.toContain('ID_'); const loaded = await loadCompatibleWorkbook(converted); const images = compatibleImages( loaded.workbook, loaded.workbook.getWorksheet('签名报备')!, loaded.wpsImagesBySheet, ); expect(images).toHaveLength(1); expect(images[0]).toMatchObject({ row: 2, column: 1, extension: 'png' }); expect(images[0].buffer.equals(PNG)).toBe(true); }); it('still rejects ordinary formulas instead of weakening spreadsheet safety', async () => { const workbook = new ExcelJS.Workbook(); workbook.addWorksheet('危险').getCell('A1').value = { formula: 'HYPERLINK("https://example.com")' }; const content = Buffer.from(await workbook.xlsx.writeBuffer()); await expect(loadCompatibleWorkbook(content)).rejects.toThrow('不允许的公式'); }); it('reads WPS image positions without inflating image buffers during analysis', async () => { const converted = await convertWorkbookOutput(await standardWorkbook(), 'wps_cell_image'); const loaded = await loadCompatibleWorkbook(converted, { includeImageData: false }); const wpsImage = loaded.wpsImagesBySheet.get('签名报备')?.[0]; expect(wpsImage).toMatchObject({ row: 2, column: 1, extension: 'png', size: PNG.length }); expect(wpsImage).not.toHaveProperty('buffer'); expect( compatibleImagePositions(loaded.workbook, loaded.workbook.getWorksheet('签名报备')!, loaded.wpsImagesBySheet), ).toEqual([{ row: 2, column: 1 }]); }); });