|
| 1 | +import { writeFile } from 'node:fs/promises' |
| 2 | +import { join, dirname } from 'node:path' |
| 3 | +import { fileURLToPath } from 'node:url' |
| 4 | + |
| 5 | +import ava, { TestFn } from 'ava' |
| 6 | + |
| 7 | +import { PDFDocument } from '../index' |
| 8 | + |
| 9 | +const __dirname = dirname(fileURLToPath(import.meta.url)) |
| 10 | + |
| 11 | +const test = ava as TestFn<{ |
| 12 | + doc: PDFDocument |
| 13 | +}> |
| 14 | + |
| 15 | +test.beforeEach((t) => { |
| 16 | + t.context.doc = new PDFDocument() |
| 17 | +}) |
| 18 | + |
| 19 | +test('should create PDF with URL link annotation', async (t) => { |
| 20 | + const { doc } = t.context |
| 21 | + const ctx = doc.beginPage(612, 792) |
| 22 | + |
| 23 | + // Draw a clickable link |
| 24 | + ctx.fillStyle = 'blue' |
| 25 | + ctx.fillRect(50, 50, 200, 40) |
| 26 | + ctx.fillStyle = 'white' |
| 27 | + ctx.font = '20px sans-serif' |
| 28 | + ctx.fillText('Click here to visit GitHub', 60, 75) |
| 29 | + |
| 30 | + // Add URL annotation |
| 31 | + ctx.annotateLinkUrl(50, 50, 250, 90, 'https://github.com/Brooooooklyn/canvas') |
| 32 | + |
| 33 | + doc.endPage() |
| 34 | + const pdfBuffer = doc.close() |
| 35 | + |
| 36 | + t.true(pdfBuffer instanceof Buffer) |
| 37 | + t.true(pdfBuffer.length > 0) |
| 38 | + t.is(pdfBuffer.toString('utf8', 0, 5), '%PDF-') |
| 39 | + |
| 40 | + // Check for annotation in PDF content |
| 41 | + const pdfContent = pdfBuffer.toString('latin1') |
| 42 | + t.true(pdfContent.includes('/Annot')) |
| 43 | + |
| 44 | + await writeFile(join(__dirname, 'pdf', 'link-annotation.pdf'), pdfBuffer) |
| 45 | +}) |
| 46 | + |
| 47 | +test('should create PDF with named destination and link to it', async (t) => { |
| 48 | + const { doc } = t.context |
| 49 | + |
| 50 | + // Page 1 - Create link to destination |
| 51 | + const ctx1 = doc.beginPage(612, 792) |
| 52 | + ctx1.fillStyle = 'black' |
| 53 | + ctx1.font = '24px sans-serif' |
| 54 | + ctx1.fillText('Table of Contents', 50, 50) |
| 55 | + |
| 56 | + // Draw a clickable link to page 2 |
| 57 | + ctx1.fillStyle = 'blue' |
| 58 | + ctx1.fillRect(50, 100, 200, 30) |
| 59 | + ctx1.fillStyle = 'white' |
| 60 | + ctx1.font = '18px sans-serif' |
| 61 | + ctx1.fillText('Go to Chapter 1', 60, 120) |
| 62 | + |
| 63 | + // Add link to named destination |
| 64 | + ctx1.annotateLinkToDestination(50, 100, 250, 130, 'chapter1') |
| 65 | + |
| 66 | + doc.endPage() |
| 67 | + |
| 68 | + // Page 2 - Create the destination |
| 69 | + const ctx2 = doc.beginPage(612, 792) |
| 70 | + |
| 71 | + // Create named destination at the top of page 2 |
| 72 | + ctx2.annotateNamedDestination(50, 50, 'chapter1') |
| 73 | + |
| 74 | + ctx2.fillStyle = 'black' |
| 75 | + ctx2.font = '30px sans-serif' |
| 76 | + ctx2.fillText('Chapter 1', 50, 100) |
| 77 | + ctx2.font = '16px sans-serif' |
| 78 | + ctx2.fillText('This is the content of chapter 1.', 50, 150) |
| 79 | + |
| 80 | + doc.endPage() |
| 81 | + |
| 82 | + const pdfBuffer = doc.close() |
| 83 | + |
| 84 | + t.true(pdfBuffer instanceof Buffer) |
| 85 | + t.true(pdfBuffer.length > 0) |
| 86 | + t.is(pdfBuffer.toString('utf8', 0, 5), '%PDF-') |
| 87 | + |
| 88 | + await writeFile(join(__dirname, 'pdf', 'named-destination.pdf'), pdfBuffer) |
| 89 | +}) |
| 90 | + |
| 91 | +test('should create PDF with multiple URL links', async (t) => { |
| 92 | + const { doc } = t.context |
| 93 | + const ctx = doc.beginPage(612, 792) |
| 94 | + |
| 95 | + ctx.fillStyle = 'black' |
| 96 | + ctx.font = '24px sans-serif' |
| 97 | + ctx.fillText('Useful Links', 50, 50) |
| 98 | + |
| 99 | + const links = [ |
| 100 | + { text: 'GitHub Repository', url: 'https://github.com/Brooooooklyn/canvas', y: 100 }, |
| 101 | + { text: 'NPM Package', url: 'https://www.npmjs.com/package/@napi-rs/canvas', y: 160 }, |
| 102 | + { text: 'Documentation', url: 'https://github.com/Brooooooklyn/canvas#readme', y: 220 }, |
| 103 | + ] |
| 104 | + |
| 105 | + links.forEach((link) => { |
| 106 | + // Draw link background |
| 107 | + ctx.fillStyle = 'lightblue' |
| 108 | + ctx.fillRect(50, link.y, 300, 40) |
| 109 | + |
| 110 | + // Draw link text |
| 111 | + ctx.fillStyle = 'darkblue' |
| 112 | + ctx.font = '18px sans-serif' |
| 113 | + ctx.fillText(link.text, 60, link.y + 25) |
| 114 | + |
| 115 | + // Add URL annotation |
| 116 | + ctx.annotateLinkUrl(50, link.y, 350, link.y + 40, link.url) |
| 117 | + }) |
| 118 | + |
| 119 | + doc.endPage() |
| 120 | + const pdfBuffer = doc.close() |
| 121 | + |
| 122 | + t.true(pdfBuffer instanceof Buffer) |
| 123 | + t.true(pdfBuffer.length > 0) |
| 124 | + |
| 125 | + await writeFile(join(__dirname, 'pdf', 'multiple-links.pdf'), pdfBuffer) |
| 126 | +}) |
| 127 | + |
| 128 | +test('should create table of contents with multiple named destinations', async (t) => { |
| 129 | + const { doc } = t.context |
| 130 | + |
| 131 | + // Table of Contents page |
| 132 | + const toc = doc.beginPage(612, 792) |
| 133 | + toc.fillStyle = 'black' |
| 134 | + toc.font = '30px sans-serif' |
| 135 | + toc.fillText('Table of Contents', 50, 50) |
| 136 | + |
| 137 | + const chapters = [ |
| 138 | + { title: 'Chapter 1: Introduction', dest: 'intro', y: 120 }, |
| 139 | + { title: 'Chapter 2: Getting Started', dest: 'getting-started', y: 160 }, |
| 140 | + { title: 'Chapter 3: Advanced Topics', dest: 'advanced', y: 200 }, |
| 141 | + { title: 'Chapter 4: Conclusion', dest: 'conclusion', y: 240 }, |
| 142 | + ] |
| 143 | + |
| 144 | + toc.font = '18px sans-serif' |
| 145 | + chapters.forEach((chapter) => { |
| 146 | + toc.fillStyle = 'blue' |
| 147 | + toc.fillText(chapter.title, 70, chapter.y) |
| 148 | + toc.annotateLinkToDestination(70, chapter.y - 20, 400, chapter.y + 5, chapter.dest) |
| 149 | + }) |
| 150 | + |
| 151 | + doc.endPage() |
| 152 | + |
| 153 | + // Create pages for each chapter |
| 154 | + chapters.forEach((chapter) => { |
| 155 | + const ctx = doc.beginPage(612, 792) |
| 156 | + ctx.annotateNamedDestination(50, 50, chapter.dest) |
| 157 | + ctx.fillStyle = 'black' |
| 158 | + ctx.font = '30px sans-serif' |
| 159 | + ctx.fillText(chapter.title.split(':')[0], 50, 100) |
| 160 | + ctx.font = '18px sans-serif' |
| 161 | + ctx.fillText(`Content of ${chapter.title.toLowerCase()}`, 50, 150) |
| 162 | + doc.endPage() |
| 163 | + }) |
| 164 | + |
| 165 | + const pdfBuffer = doc.close() |
| 166 | + |
| 167 | + t.true(pdfBuffer instanceof Buffer) |
| 168 | + t.true(pdfBuffer.length > 0) |
| 169 | + |
| 170 | + await writeFile(join(__dirname, 'pdf', 'toc-with-destinations.pdf'), pdfBuffer) |
| 171 | +}) |
| 172 | + |
| 173 | +test('should handle empty string URL gracefully', (t) => { |
| 174 | + const { doc } = t.context |
| 175 | + const ctx = doc.beginPage(612, 792) |
| 176 | + |
| 177 | + ctx.fillStyle = 'black' |
| 178 | + ctx.fillRect(50, 50, 100, 100) |
| 179 | + |
| 180 | + // Should not crash with empty URL |
| 181 | + t.notThrows(() => { |
| 182 | + ctx.annotateLinkUrl(50, 50, 150, 150, '') |
| 183 | + }) |
| 184 | + |
| 185 | + doc.endPage() |
| 186 | + const pdfBuffer = doc.close() |
| 187 | + |
| 188 | + t.true(pdfBuffer instanceof Buffer) |
| 189 | +}) |
| 190 | + |
| 191 | +test('should handle annotations with special characters in URL', async (t) => { |
| 192 | + const { doc } = t.context |
| 193 | + const ctx = doc.beginPage(612, 792) |
| 194 | + |
| 195 | + ctx.fillStyle = 'blue' |
| 196 | + ctx.fillRect(50, 50, 300, 40) |
| 197 | + ctx.fillStyle = 'white' |
| 198 | + ctx.font = '16px sans-serif' |
| 199 | + ctx.fillText('Link with special chars', 60, 75) |
| 200 | + |
| 201 | + // URL with query parameters and special characters |
| 202 | + const specialUrl = 'https://example.com/search?q=test&lang=en&special=äöü' |
| 203 | + ctx.annotateLinkUrl(50, 50, 350, 90, specialUrl) |
| 204 | + |
| 205 | + doc.endPage() |
| 206 | + const pdfBuffer = doc.close() |
| 207 | + |
| 208 | + t.true(pdfBuffer instanceof Buffer) |
| 209 | + t.true(pdfBuffer.length > 0) |
| 210 | + |
| 211 | + await writeFile(join(__dirname, 'pdf', 'special-chars-link.pdf'), pdfBuffer) |
| 212 | +}) |
| 213 | + |
| 214 | +test('should support annotations on rotated/transformed canvas', async (t) => { |
| 215 | + const { doc } = t.context |
| 216 | + const ctx = doc.beginPage(612, 792) |
| 217 | + |
| 218 | + // Draw without transformation |
| 219 | + ctx.fillStyle = 'red' |
| 220 | + ctx.fillRect(50, 50, 100, 40) |
| 221 | + ctx.annotateLinkUrl(50, 50, 150, 90, 'https://example.com/normal') |
| 222 | + |
| 223 | + // Apply transformation and draw |
| 224 | + ctx.save() |
| 225 | + ctx.translate(300, 300) |
| 226 | + ctx.rotate((45 * Math.PI) / 180) |
| 227 | + |
| 228 | + ctx.fillStyle = 'blue' |
| 229 | + ctx.fillRect(-50, -20, 100, 40) |
| 230 | + // Note: Annotation coordinates should be in the transformed space |
| 231 | + ctx.annotateLinkUrl(-50, -20, 50, 20, 'https://example.com/rotated') |
| 232 | + |
| 233 | + ctx.restore() |
| 234 | + |
| 235 | + doc.endPage() |
| 236 | + const pdfBuffer = doc.close() |
| 237 | + |
| 238 | + t.true(pdfBuffer instanceof Buffer) |
| 239 | + t.true(pdfBuffer.length > 0) |
| 240 | + |
| 241 | + await writeFile(join(__dirname, 'pdf', 'transformed-annotations.pdf'), pdfBuffer) |
| 242 | +}) |
0 commit comments