pdf-lite - v1.0.1
    Preparing search index...

    PDF-Lite Examples

    This directory contains example scripts demonstrating how to use the PDF-Lite library.

    import { writeFileSync } from 'fs'
    import { PdfArray } from 'pdf-lite/core/objects/pdf-array'
    import { PdfDictionary } from 'pdf-lite/core/objects/pdf-dictionary'
    import { PdfIndirectObject } from 'pdf-lite/core/objects/pdf-indirect-object'
    import { PdfName } from 'pdf-lite/core/objects/pdf-name'
    import { PdfNumber } from 'pdf-lite/core/objects/pdf-number'
    import { PdfObjectReference } from 'pdf-lite/core/objects/pdf-object-reference'
    import { PdfStream } from 'pdf-lite/core/objects/pdf-stream'
    import { PdfDocument } from 'pdf-lite/pdf/pdf-document'

    function createPage(
    contentStreamRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const pageDict = new PdfDictionary()
    pageDict.set('Type', new PdfName('Page'))
    pageDict.set(
    'MediaBox',
    new PdfArray([
    new PdfNumber(0),
    new PdfNumber(0),
    new PdfNumber(612),
    new PdfNumber(792),
    ]),
    )
    pageDict.set('Contents', contentStreamRef)
    return new PdfIndirectObject({ content: pageDict })
    }

    function createPages(
    pages: PdfIndirectObject<PdfDictionary>[],
    ): PdfIndirectObject<PdfDictionary> {
    const pagesDict = new PdfDictionary()
    pagesDict.set('Type', new PdfName('Pages'))
    pagesDict.set('Kids', new PdfArray(pages.map((x) => x.reference)))
    pagesDict.set('Count', new PdfNumber(pages.length))
    return new PdfIndirectObject({ content: pagesDict })
    }

    function createCatalog(
    pagesRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const catalogDict = new PdfDictionary()
    catalogDict.set('Type', new PdfName('Catalog'))
    catalogDict.set('Pages', pagesRef)
    return new PdfIndirectObject({ content: catalogDict })
    }

    function createFont(): PdfIndirectObject<PdfDictionary> {
    const fontDict = new PdfDictionary()
    fontDict.set('Type', new PdfName('Font'))
    fontDict.set('Subtype', new PdfName('Type1'))
    fontDict.set('BaseFont', new PdfName('Helvetica'))
    return new PdfIndirectObject({ content: fontDict })
    }

    function createResources(
    fontRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const resourcesDict = new PdfDictionary()
    const fontDict = new PdfDictionary()
    fontDict.set('F1', fontRef)
    resourcesDict.set('Font', fontDict)
    return new PdfIndirectObject({ content: resourcesDict })
    }

    // Create the document
    const document = new PdfDocument()

    // Create font
    const font = createFont()
    document.add(font)

    // Create resources with the font
    const resources = createResources(font.reference)
    document.add(resources)

    // Create content stream
    const contentStream = new PdfIndirectObject({
    content: new PdfStream({
    header: new PdfDictionary(),
    original: 'BT /F1 24 Tf 100 700 Td (Hello, PDF-Lite!) Tj ET',
    }),
    })

    // Create a page
    const page = createPage(contentStream.reference)
    // Add resources to the page
    page.content.set('Resources', resources.reference)
    document.add(page)

    // Create pages collection
    const pages = createPages([page])
    // Set parent reference for the page
    page.content.set('Parent', pages.reference)
    document.add(pages)

    // Create catalog
    const catalog = createCatalog(pages.reference)
    document.add(catalog)

    // Set the catalog as the root
    document.trailerDict.set('Root', catalog.reference)

    document.add(contentStream)
    await document.commit()

    const file = `${import.meta.dirname}/tmp/created.pdf`
    console.log(`Writing PDF to: ${file}`)

    await writeFileSync(`${file}`, document.toBytes())
    import { writeFileSync } from 'fs'
    import { PdfArray } from 'pdf-lite/core/objects/pdf-array'
    import { PdfDictionary } from 'pdf-lite/core/objects/pdf-dictionary'
    import { PdfIndirectObject } from 'pdf-lite/core/objects/pdf-indirect-object'
    import { PdfName } from 'pdf-lite/core/objects/pdf-name'
    import { PdfNumber } from 'pdf-lite/core/objects/pdf-number'
    import { PdfObjectReference } from 'pdf-lite/core/objects/pdf-object-reference'
    import { PdfStream } from 'pdf-lite/core/objects/pdf-stream'
    import { PdfDocument } from 'pdf-lite/pdf/pdf-document'
    import { PdfV2SecurityHandler } from 'pdf-lite/security/handlers/v2'

    function createPage(
    contentStreamRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const pageDict = new PdfDictionary()
    pageDict.set('Type', new PdfName('Page'))
    pageDict.set(
    'MediaBox',
    new PdfArray([
    new PdfNumber(0),
    new PdfNumber(0),
    new PdfNumber(612),
    new PdfNumber(792),
    ]),
    )
    pageDict.set('Contents', contentStreamRef)
    return new PdfIndirectObject({ content: pageDict })
    }

    function createPages(
    pages: PdfIndirectObject<PdfDictionary>[],
    ): PdfIndirectObject<PdfDictionary> {
    const pagesDict = new PdfDictionary()
    pagesDict.set('Type', new PdfName('Pages'))
    pagesDict.set('Kids', new PdfArray(pages.map((x) => x.reference)))
    pagesDict.set('Count', new PdfNumber(pages.length))
    return new PdfIndirectObject({ content: pagesDict })
    }

    function createCatalog(
    pagesRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const catalogDict = new PdfDictionary()
    catalogDict.set('Type', new PdfName('Catalog'))
    catalogDict.set('Pages', pagesRef)
    return new PdfIndirectObject({ content: catalogDict })
    }

    function createFont(): PdfIndirectObject<PdfDictionary> {
    const fontDict = new PdfDictionary()
    fontDict.set('Type', new PdfName('Font'))
    fontDict.set('Subtype', new PdfName('Type1'))
    fontDict.set('BaseFont', new PdfName('Helvetica'))
    return new PdfIndirectObject({ content: fontDict })
    }

    function createResources(
    fontRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const resourcesDict = new PdfDictionary()
    const fontDict = new PdfDictionary()
    fontDict.set('F1', fontRef)
    resourcesDict.set('Font', fontDict)
    return new PdfIndirectObject({ content: resourcesDict })
    }

    // Create the document
    const document = new PdfDocument()

    // Create font
    const font = createFont()
    document.add(font)

    // Create resources with the font
    const resources = createResources(font.reference)
    document.add(resources)

    // Create content stream
    const contentStream = new PdfIndirectObject({
    content: new PdfStream({
    header: new PdfDictionary(),
    original: 'BT /F1 24 Tf 100 700 Td (Hello, PDF-Lite!) Tj ET',
    }),
    })

    // Create a page
    const page = createPage(contentStream.reference)
    // Add resources to the page
    page.content.set('Resources', resources.reference)
    document.add(page)

    // Create pages collection
    const pages = createPages([page])
    // Set parent reference for the page
    page.content.set('Parent', pages.reference)
    document.add(pages)

    // Create catalog
    const catalog = createCatalog(pages.reference)
    document.add(catalog)

    // Set the catalog as the root
    document.trailerDict.set('Root', catalog.reference)

    document.add(contentStream)
    await document.commit()

    document.securityHandler = new PdfV2SecurityHandler({
    password: 'up',
    documentId: 'cafebabe',
    encryptMetadata: true,
    })

    await document.encrypt()

    const file = `${import.meta.dirname}/tmp/encrypted.pdf`
    console.log(`Writing encrypted PDF to: ${file}. Password: "up"`)

    await writeFileSync(`${file}`, document.toBytes())
    import { PdfArray } from 'pdf-lite/core/objects/pdf-array'
    import { PdfDictionary } from 'pdf-lite/core/objects/pdf-dictionary'
    import { PdfIndirectObject } from 'pdf-lite/core/objects/pdf-indirect-object'
    import { PdfName } from 'pdf-lite/core/objects/pdf-name'
    import { PdfNumber } from 'pdf-lite/core/objects/pdf-number'
    import { PdfObjectReference } from 'pdf-lite/core/objects/pdf-object-reference'
    import { PdfStream } from 'pdf-lite/core/objects/pdf-stream'
    import { PdfDocument } from 'pdf-lite/pdf/pdf-document'
    import { PdfString } from 'pdf-lite/core/objects/pdf-string'
    import {
    PdfAdbePkcsX509RsaSha1SignatureObject,
    PdfAdbePkcs7DetachedSignatureObject,
    PdfAdbePkcs7Sha1SignatureObject,
    PdfEtsiCadesDetachedSignatureObject,
    PdfEtsiRfc3161SignatureObject,
    } from 'pdf-lite'
    import { rsaSigningKeys } from '../packages/pdf-lite/test/unit/fixtures/rsa-2048/index'
    import fs from 'fs/promises'

    function createPage(
    contentStreamRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const pageDict = new PdfDictionary()
    pageDict.set('Type', new PdfName('Page'))
    pageDict.set(
    'MediaBox',
    new PdfArray([
    new PdfNumber(0),
    new PdfNumber(0),
    new PdfNumber(612),
    new PdfNumber(792),
    ]),
    )
    pageDict.set('Contents', contentStreamRef)
    return new PdfIndirectObject({ content: pageDict })
    }

    function createPages(
    pages: PdfIndirectObject<PdfDictionary>[],
    ): PdfIndirectObject<PdfDictionary> {
    const pagesDict = new PdfDictionary()
    pagesDict.set('Type', new PdfName('Pages'))
    pagesDict.set('Kids', new PdfArray(pages.map((x) => x.reference)))
    pagesDict.set('Count', new PdfNumber(pages.length))
    return new PdfIndirectObject({ content: pagesDict })
    }

    function createCatalog(
    pagesRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const catalogDict = new PdfDictionary()
    catalogDict.set('Type', new PdfName('Catalog'))
    catalogDict.set('Pages', pagesRef)
    return new PdfIndirectObject({ content: catalogDict })
    }

    function createFont(): PdfIndirectObject<PdfDictionary> {
    const fontDict = new PdfDictionary()
    fontDict.set('Type', new PdfName('Font'))
    fontDict.set('Subtype', new PdfName('Type1'))
    fontDict.set('BaseFont', new PdfName('Helvetica'))
    return new PdfIndirectObject({ content: fontDict })
    }

    function createResources(
    fontRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const resourcesDict = new PdfDictionary()
    const fontDict = new PdfDictionary()
    fontDict.set('F1', fontRef)
    resourcesDict.set('Font', fontDict)
    return new PdfIndirectObject({ content: resourcesDict })
    }

    function createPageWithSignatureField(
    contentStreamRef: PdfObjectReference,
    signatureAnnotRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const pageDict = new PdfDictionary()
    pageDict.set('Type', new PdfName('Page'))
    pageDict.set(
    'MediaBox',
    new PdfArray([
    new PdfNumber(0),
    new PdfNumber(0),
    new PdfNumber(612),
    new PdfNumber(792),
    ]),
    )
    pageDict.set('Contents', contentStreamRef)
    pageDict.set('Annots', new PdfArray([signatureAnnotRef]))

    return new PdfIndirectObject({ content: pageDict })
    }

    function createSignatureAnnotation(
    signatureRef: PdfObjectReference,
    appearanceStreamRef: PdfObjectReference,
    pageRef: PdfObjectReference,
    signatureName: string,
    ): PdfIndirectObject<PdfDictionary> {
    const signatureAnnotation = new PdfDictionary()
    signatureAnnotation.set('Type', new PdfName('Annot'))
    signatureAnnotation.set('Subtype', new PdfName('Widget'))
    signatureAnnotation.set('FT', new PdfName('Sig'))
    signatureAnnotation.set('T', new PdfString(signatureName))
    signatureAnnotation.set(
    'Rect',
    new PdfArray([
    new PdfNumber(135), // x1: Start after "Signature: " text (~72 + 63)
    new PdfNumber(640), // y1: Bottom of signature area (652 - 12)
    new PdfNumber(400), // x2: End of signature line
    new PdfNumber(665), // y2: Top of signature area (652 + 13)
    ]),
    )
    signatureAnnotation.set('F', new PdfNumber(4))
    signatureAnnotation.set('P', pageRef) // Reference to parent page
    signatureAnnotation.set('V', signatureRef)

    // Add appearance dictionary
    const appearanceDict = new PdfDictionary()
    appearanceDict.set('N', appearanceStreamRef)
    signatureAnnotation.set('AP', appearanceDict)

    return new PdfIndirectObject({ content: signatureAnnotation })
    }

    function createSignatureAppearance(): PdfIndirectObject<PdfStream> {
    // Create font for appearance
    const appearanceFont = new PdfDictionary()
    appearanceFont.set('Type', new PdfName('Font'))
    appearanceFont.set('Subtype', new PdfName('Type1'))
    appearanceFont.set('BaseFont', new PdfName('Helvetica'))

    const fontDict = new PdfDictionary()
    fontDict.set('F1', appearanceFont)

    const resourcesDict = new PdfDictionary()
    resourcesDict.set('Font', fontDict)

    // Create appearance stream header
    const appearanceHeader = new PdfDictionary()
    appearanceHeader.set('Type', new PdfName('XObject'))
    appearanceHeader.set('Subtype', new PdfName('Form'))
    appearanceHeader.set(
    'BBox',
    new PdfArray([
    new PdfNumber(0),
    new PdfNumber(0),
    new PdfNumber(265), // Width: 400 - 135
    new PdfNumber(25), // Height: 665 - 640
    ]),
    )
    appearanceHeader.set('Resources', resourcesDict)

    // Create appearance stream for the signature
    return new PdfIndirectObject({
    content: new PdfStream({
    header: appearanceHeader,
    original:
    'BT /F1 10 Tf 5 14 Td (Digitally signed by: Jake Shirley) Tj ET',
    }),
    })
    }

    // Create the document
    const document = new PdfDocument()

    // Create font
    const font = createFont()
    document.add(font)

    // Create resources with the font
    const resources = createResources(font.reference)
    document.add(resources)

    // Create content stream for first page
    const contentStream = new PdfIndirectObject({
    content: new PdfStream({
    header: new PdfDictionary(),
    original: 'BT /F1 24 Tf 100 700 Td (Hello, PDF-Lite!) Tj ET',
    }),
    })
    document.add(contentStream)

    // Create first page
    const page1 = createPage(contentStream.reference)
    page1.content.set('Resources', resources.reference)
    document.add(page1)

    // Array to hold all pages and signature objects
    const allPages: PdfIndirectObject<PdfDictionary>[] = [page1]
    const allSignatures: any[] = []
    const signatureFields: PdfObjectReference[] = []

    // Helper function to create a signature page
    function createSignaturePage(
    signatureType: string,
    signatureObj: any,
    pageNumber: number,
    ) {
    const content = new PdfIndirectObject({
    content: new PdfStream({
    header: new PdfDictionary(),
    original: `BT /F1 12 Tf 72 712 Td (Signature Type: ${signatureType}) Tj 0 -60 Td (Signature: ________________________________) Tj ET`,
    }),
    })
    document.add(content)

    const appearance = createSignatureAppearance()
    document.add(appearance)

    // Create page first to get its reference
    const page = createPageWithSignatureField(
    content.reference,
    new PdfObjectReference(0, 0), // Temporary placeholder
    )
    page.content.set('Resources', resources.reference)
    document.add(page)

    // Now create annotation with page reference
    const annotation = createSignatureAnnotation(
    signatureObj.reference,
    appearance.reference,
    page.reference,
    `Signature${pageNumber}`,
    )
    document.add(annotation)

    // Update page's Annots array with actual annotation reference
    page.content.set('Annots', new PdfArray([annotation.reference]))

    signatureFields.push(annotation.reference)
    return page
    }

    // Page 2: Adobe PKCS7 Detached
    const pkcs7DetachedSig = new PdfAdbePkcs7DetachedSignatureObject({
    privateKey: rsaSigningKeys.privateKey,
    certificate: rsaSigningKeys.cert,
    issuerCertificate: rsaSigningKeys.caCert,
    name: 'Jake Shirley',
    location: 'Earth',
    reason: 'PKCS7 Detached Signature',
    contactInfo: 'test@test.com',
    revocationInfo: {
    crls: [rsaSigningKeys.caCrl],
    ocsps: [rsaSigningKeys.ocspResponse],
    },
    })
    allSignatures.push(pkcs7DetachedSig)
    allPages.push(createSignaturePage('Adobe PKCS7 Detached', pkcs7DetachedSig, 2))

    // Page 3: Adobe PKCS7 SHA1
    const pkcs7Sha1Sig = new PdfAdbePkcs7Sha1SignatureObject({
    privateKey: rsaSigningKeys.privateKey,
    certificate: rsaSigningKeys.cert,
    issuerCertificate: rsaSigningKeys.caCert,
    name: 'Jake Shirley',
    location: 'Earth',
    reason: 'PKCS7 SHA1 Signature',
    contactInfo: 'test@test.com',
    })
    allSignatures.push(pkcs7Sha1Sig)
    allPages.push(createSignaturePage('Adobe PKCS7 SHA1', pkcs7Sha1Sig, 3))

    // Page 4: Adobe X509 RSA SHA1
    const x509RsaSha1Sig = new PdfAdbePkcsX509RsaSha1SignatureObject({
    privateKey: rsaSigningKeys.privateKey,
    certificate: rsaSigningKeys.cert,
    additionalCertificates: [rsaSigningKeys.caCert],
    name: 'Jake Shirley',
    location: 'Earth',
    reason: 'X509 RSA SHA1 Signature',
    contactInfo: 'test@test.com',
    revocationInfo: {
    crls: [rsaSigningKeys.caCrl],
    ocsps: [rsaSigningKeys.ocspResponse],
    },
    })
    allSignatures.push(x509RsaSha1Sig)
    allPages.push(createSignaturePage('Adobe X509 RSA SHA1', x509RsaSha1Sig, 4))

    // Page 5: ETSI CAdES Detached
    const cadesDetachedSig = new PdfEtsiCadesDetachedSignatureObject({
    privateKey: rsaSigningKeys.privateKey,
    certificate: rsaSigningKeys.cert,
    issuerCertificate: rsaSigningKeys.caCert,
    name: 'Jake Shirley',
    location: 'Earth',
    reason: 'CAdES Detached Signature',
    contactInfo: 'test@test.com',
    revocationInfo: {
    crls: [rsaSigningKeys.caCrl],
    ocsps: [rsaSigningKeys.ocspResponse],
    },
    })
    allSignatures.push(cadesDetachedSig)
    allPages.push(createSignaturePage('ETSI CAdES Detached', cadesDetachedSig, 5))

    // Page 6: ETSI RFC3161 (Timestamp)
    const rfc3161Sig = new PdfEtsiRfc3161SignatureObject({
    timeStampAuthority: {
    url: 'https://freetsa.org/tsr',
    },
    })
    allSignatures.push(rfc3161Sig)
    allPages.push(createSignaturePage('ETSI RFC3161 Timestamp', rfc3161Sig, 6))

    // Create pages collection with all pages
    const pages = createPages(allPages)
    // Set parent reference for all pages
    allPages.forEach((page) => {
    page.content.set('Parent', pages.reference)
    })
    document.add(pages)

    // Create catalog with AcroForm
    const catalog = createCatalog(pages.reference)

    // Add AcroForm to catalog with all signature fields
    const acroForm = new PdfDictionary()
    acroForm.set('Fields', new PdfArray(signatureFields))
    acroForm.set('SigFlags', new PdfNumber(3))
    const acroFormObj = new PdfIndirectObject({ content: acroForm })
    document.add(acroFormObj)
    catalog.content.set('AcroForm', acroFormObj.reference)

    document.add(catalog)

    // Set the catalog as the root
    document.trailerDict.set('Root', catalog.reference)

    // IMPORTANT: Add all signatures LAST - after all other objects
    // This ensures the ByteRange is calculated correctly for each signature
    allSignatures.forEach((sig) => {
    document.startNewRevision()
    document.add(sig)
    })

    await document.commit()

    const tmpFolder = `${import.meta.dirname}/tmp`
    await fs.mkdir(tmpFolder, { recursive: true })
    await fs.writeFile(`${tmpFolder}/signed-output.pdf`, document.toBytes())
    import { PdfArray } from 'pdf-lite/core/objects/pdf-array'
    import { PdfDictionary } from 'pdf-lite/core/objects/pdf-dictionary'
    import { PdfIndirectObject } from 'pdf-lite/core/objects/pdf-indirect-object'
    import { PdfName } from 'pdf-lite/core/objects/pdf-name'
    import { PdfNumber } from 'pdf-lite/core/objects/pdf-number'
    import { PdfObjectReference } from 'pdf-lite/core/objects/pdf-object-reference'
    import { PdfStream } from 'pdf-lite/core/objects/pdf-stream'
    import { PdfDocument } from 'pdf-lite/pdf/pdf-document'
    import fs from 'fs/promises'

    const tmpFolder = `${import.meta.dirname}/tmp`
    await fs.mkdir(tmpFolder, { recursive: true })

    // Helper functions for creating PDF objects
    function createPage(
    contentStreamRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const pageDict = new PdfDictionary()
    pageDict.set('Type', new PdfName('Page'))
    pageDict.set(
    'MediaBox',
    new PdfArray([
    new PdfNumber(0),
    new PdfNumber(0),
    new PdfNumber(612),
    new PdfNumber(792),
    ]),
    )
    pageDict.set('Contents', contentStreamRef)
    return new PdfIndirectObject({ content: pageDict })
    }

    function createPages(
    pages: PdfIndirectObject<PdfDictionary>[],
    ): PdfIndirectObject<PdfDictionary> {
    const pagesDict = new PdfDictionary()
    pagesDict.set('Type', new PdfName('Pages'))
    pagesDict.set('Kids', new PdfArray(pages.map((x) => x.reference)))
    pagesDict.set('Count', new PdfNumber(pages.length))
    return new PdfIndirectObject({ content: pagesDict })
    }

    function createCatalog(
    pagesRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const catalogDict = new PdfDictionary()
    catalogDict.set('Type', new PdfName('Catalog'))
    catalogDict.set('Pages', pagesRef)
    return new PdfIndirectObject({ content: catalogDict })
    }

    function createFont(): PdfIndirectObject<PdfDictionary> {
    const fontDict = new PdfDictionary()
    fontDict.set('Type', new PdfName('Font'))
    fontDict.set('Subtype', new PdfName('Type1'))
    fontDict.set('BaseFont', new PdfName('Helvetica'))
    return new PdfIndirectObject({ content: fontDict })
    }

    function createResources(
    fontRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const resourcesDict = new PdfDictionary()
    const fontDict = new PdfDictionary()
    fontDict.set('F1', fontRef)
    resourcesDict.set('Font', fontDict)
    return new PdfIndirectObject({ content: resourcesDict })
    }

    // Step 1: Create an initial PDF document
    console.log('Step 1: Creating initial PDF document...')
    const document = new PdfDocument()

    const font = createFont()
    document.add(font)
    const resources = createResources(font.reference)
    document.add(resources)

    const contentStream = new PdfIndirectObject({
    content: new PdfStream({
    header: new PdfDictionary(),
    original:
    'BT /F1 24 Tf 100 700 Td (Original Document - Revision 1) Tj ET',
    }),
    })

    const page = createPage(contentStream.reference)
    page.content.set('Resources', resources.reference)
    document.add(page)

    const pages = createPages([page])
    page.content.set('Parent', pages.reference)
    document.add(pages)

    const catalog = createCatalog(pages.reference)
    document.add(catalog)

    document.trailerDict.set('Root', catalog.reference)
    document.add(contentStream)

    await document.commit()
    // Save the original PDF
    const originalPdfPath = `${tmpFolder}/original.pdf`
    await fs.writeFile(originalPdfPath, document.toBytes())
    console.log(`Original PDF saved to: ${originalPdfPath}`)
    console.log(`Original PDF has ${document.revisions.length} revision(s)`)

    // Step 2: Load the PDF and perform an incremental update
    console.log('\nStep 2: Loading PDF and performing incremental update...')

    // Read the existing PDF
    const existingPdfBytes = await fs.readFile(originalPdfPath)
    const loadedDocument = await PdfDocument.fromBytes([existingPdfBytes])

    // Lock existing revisions to enable incremental mode
    // This ensures changes are added as new revisions instead of modifying existing ones
    loadedDocument.setIncremental(true)

    // Create new content for the incremental update
    // In a real scenario, this could be adding annotations, form fields, signatures, etc.
    const newContentStream = new PdfIndirectObject({
    objectNumber: contentStream.objectNumber,
    generationNumber: contentStream.generationNumber,
    content: new PdfStream({
    header: new PdfDictionary(),
    original:
    'BT /F1 18 Tf 100 650 Td (This content was added in Revision 2) Tj ET',
    }),
    })

    // Add the new content to the document
    loadedDocument.add(newContentStream)
    await loadedDocument.commit()

    // Save the incrementally updated PDF
    const updatedPdfPath = `${tmpFolder}/incremental-update.pdf`
    await fs.writeFile(updatedPdfPath, loadedDocument.toBytes())
    console.log(`Incrementally updated PDF saved to: ${updatedPdfPath}`)
    console.log(`Updated PDF has ${loadedDocument.revisions.length} revision(s)`)

    // Step 3: Verify the incremental update preserved the original content
    console.log('\nStep 3: Verifying incremental update...')

    // Check file sizes to confirm incremental update (new file should be larger)
    const originalStats = await fs.stat(originalPdfPath)
    const updatedStats = await fs.stat(updatedPdfPath)

    console.log(`Original PDF size: ${originalStats.size} bytes`)
    console.log(`Updated PDF size: ${updatedStats.size} bytes`)
    console.log(
    `Size difference: ${updatedStats.size - originalStats.size} bytes (new revision data)`,
    )

    // The updated PDF contains the original bytes plus the new revision
    // This is the key feature of incremental updates - the original PDF is preserved
    const updatedPdfBytes = await fs.readFile(updatedPdfPath)
    const originalPdfBytesForComparison = await fs.readFile(originalPdfPath)

    // Verify that the beginning of the updated PDF matches the original
    const originalBytesMatch = updatedPdfBytes
    .slice(0, originalPdfBytesForComparison.length - 10) // Exclude the %%EOF marker area
    .toString()
    .includes(
    originalPdfBytesForComparison
    .subarray(0, -10)
    .toString()
    .substring(0, 100),
    )

    console.log(`Original content preserved: ${originalBytesMatch ? 'Yes' : 'No'}`)

    // Step 4: Add another incremental revision
    console.log('\nStep 4: Adding another incremental revision...')

    const secondUpdate = await PdfDocument.fromBytes([updatedPdfBytes])
    secondUpdate.setIncremental(true)

    const thirdRevisionContent = new PdfIndirectObject({
    objectNumber: contentStream.objectNumber,
    generationNumber: contentStream.generationNumber,
    content: new PdfStream(
    'BT /F1 14 Tf 100 600 Td (Third revision - demonstrates multiple incremental updates) Tj ET',
    ),
    })

    secondUpdate.add(thirdRevisionContent)
    await secondUpdate.commit()

    const multiRevisionPdfPath = `${tmpFolder}/multi-revision.pdf`
    await fs.writeFile(multiRevisionPdfPath, secondUpdate.toBytes())
    console.log(`Multi-revision PDF saved to: ${multiRevisionPdfPath}`)
    console.log(
    `Multi-revision PDF has ${secondUpdate.revisions.length} revision(s)`,
    )

    const multiRevisionStats = await fs.stat(multiRevisionPdfPath)
    console.log(`Multi-revision PDF size: ${multiRevisionStats.size} bytes`)

    console.log('\n=== Summary ===')
    console.log('Incremental updates allow you to:')
    console.log('1. Preserve the original PDF content (important for signatures)')
    console.log('2. Add new content without modifying existing revisions')
    console.log('3. Maintain a complete history of document changes')
    console.log('4. Stack multiple revisions on top of each other')
    import { PdfArray } from 'pdf-lite/core/objects/pdf-array'
    import { PdfBoolean } from 'pdf-lite/core/objects/pdf-boolean'
    import { PdfDictionary } from 'pdf-lite/core/objects/pdf-dictionary'
    import { PdfIndirectObject } from 'pdf-lite/core/objects/pdf-indirect-object'
    import { PdfName } from 'pdf-lite/core/objects/pdf-name'
    import { PdfNumber } from 'pdf-lite/core/objects/pdf-number'
    import { PdfObjectReference } from 'pdf-lite/core/objects/pdf-object-reference'
    import { PdfStream } from 'pdf-lite/core/objects/pdf-stream'
    import { PdfString } from 'pdf-lite/core/objects/pdf-string'
    import { PdfDocument } from 'pdf-lite/pdf/pdf-document'
    import fs from 'fs/promises'

    const tmpFolder = `${import.meta.dirname}/tmp`
    await fs.mkdir(tmpFolder, { recursive: true })

    // Helper function to create a basic page
    function createPage(
    contentStreamRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const pageDict = new PdfDictionary()
    pageDict.set('Type', new PdfName('Page'))
    pageDict.set(
    'MediaBox',
    new PdfArray([
    new PdfNumber(0),
    new PdfNumber(0),
    new PdfNumber(612),
    new PdfNumber(792),
    ]),
    )
    pageDict.set('Contents', contentStreamRef)
    return new PdfIndirectObject({ content: pageDict })
    }

    // Helper function to create pages collection
    function createPages(
    pages: PdfIndirectObject<PdfDictionary>[],
    ): PdfIndirectObject<PdfDictionary> {
    const pagesDict = new PdfDictionary()
    pagesDict.set('Type', new PdfName('Pages'))
    pagesDict.set('Kids', new PdfArray(pages.map((x) => x.reference)))
    pagesDict.set('Count', new PdfNumber(pages.length))
    return new PdfIndirectObject({ content: pagesDict })
    }

    // Helper function to create catalog
    function createCatalog(
    pagesRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const catalogDict = new PdfDictionary()
    catalogDict.set('Type', new PdfName('Catalog'))
    catalogDict.set('Pages', pagesRef)
    return new PdfIndirectObject({ content: catalogDict })
    }

    // Helper function to create font
    function createFont(): PdfIndirectObject<PdfDictionary> {
    const fontDict = new PdfDictionary()
    fontDict.set('Type', new PdfName('Font'))
    fontDict.set('Subtype', new PdfName('Type1'))
    fontDict.set('BaseFont', new PdfName('Helvetica'))
    return new PdfIndirectObject({ content: fontDict })
    }

    // Helper function to create resources
    function createResources(
    fontRef: PdfObjectReference,
    ): PdfIndirectObject<PdfDictionary> {
    const resourcesDict = new PdfDictionary()
    const fontDict = new PdfDictionary()
    fontDict.set('F1', fontRef)
    resourcesDict.set('Font', fontDict)
    return new PdfIndirectObject({ content: resourcesDict })
    }

    // Helper function to create a text field widget annotation
    function createTextField(
    fieldName: string,
    pageRef: PdfObjectReference,
    rect: [number, number, number, number],
    defaultValue: string = '',
    ): PdfIndirectObject<PdfDictionary> {
    const fieldDict = new PdfDictionary()
    // Annotation properties
    fieldDict.set('Type', new PdfName('Annot'))
    fieldDict.set('Subtype', new PdfName('Widget'))
    // Field type: Text
    fieldDict.set('FT', new PdfName('Tx'))
    // Field name
    fieldDict.set('T', new PdfString(fieldName))
    // Bounding rectangle [x1, y1, x2, y2]
    fieldDict.set(
    'Rect',
    new PdfArray([
    new PdfNumber(rect[0]),
    new PdfNumber(rect[1]),
    new PdfNumber(rect[2]),
    new PdfNumber(rect[3]),
    ]),
    )
    // Annotation flags (4 = print)
    fieldDict.set('F', new PdfNumber(4))
    // Parent page reference
    fieldDict.set('P', pageRef)
    // Default value (if any)
    if (defaultValue) {
    fieldDict.set('V', new PdfString(defaultValue))
    fieldDict.set('DV', new PdfString(defaultValue))
    }
    // Default appearance string (font and size)
    fieldDict.set('DA', new PdfString('/Helv 12 Tf 0 g'))

    return new PdfIndirectObject({ content: fieldDict })
    }

    // Helper function to create a checkbox field widget annotation
    function createCheckboxField(
    fieldName: string,
    pageRef: PdfObjectReference,
    rect: [number, number, number, number],
    checked: boolean = false,
    ): PdfIndirectObject<PdfDictionary> {
    const fieldDict = new PdfDictionary()
    // Annotation properties
    fieldDict.set('Type', new PdfName('Annot'))
    fieldDict.set('Subtype', new PdfName('Widget'))
    // Field type: Button
    fieldDict.set('FT', new PdfName('Btn'))
    // Field name
    fieldDict.set('T', new PdfString(fieldName))
    // Bounding rectangle
    fieldDict.set(
    'Rect',
    new PdfArray([
    new PdfNumber(rect[0]),
    new PdfNumber(rect[1]),
    new PdfNumber(rect[2]),
    new PdfNumber(rect[3]),
    ]),
    )
    // Annotation flags (4 = print)
    fieldDict.set('F', new PdfNumber(4))
    // Parent page reference
    fieldDict.set('P', pageRef)
    // Value: /Yes for checked, /Off for unchecked
    fieldDict.set('V', new PdfName(checked ? 'Yes' : 'Off'))
    fieldDict.set('AS', new PdfName(checked ? 'Yes' : 'Off'))

    return new PdfIndirectObject({ content: fieldDict })
    }

    // ============================================
    // PART 1: Create a PDF with form fields
    // ============================================

    const document = new PdfDocument()

    // Create font
    const font = createFont()
    document.add(font)

    // Create resources with the font
    const resources = createResources(font.reference)
    document.add(resources)

    // Create content stream with form labels
    const contentStream = new PdfIndirectObject({
    content: new PdfStream({
    header: new PdfDictionary(),
    original: `BT
    /F1 18 Tf 72 720 Td (PDF Form Example) Tj
    /F1 12 Tf 0 -40 Td (Name:) Tj
    0 -30 Td (Email:) Tj
    0 -30 Td (Phone:) Tj
    0 -30 Td (Subscribe to newsletter:) Tj
    ET`,
    }),
    })
    document.add(contentStream)

    // Create page
    const page = createPage(contentStream.reference)
    page.content.set('Resources', resources.reference)
    document.add(page)

    // Create form fields
    const nameField = createTextField('name', page.reference, [150, 665, 400, 685])
    const emailField = createTextField(
    'email',
    page.reference,
    [150, 635, 400, 655],
    )
    const phoneField = createTextField(
    'phone',
    page.reference,
    [150, 605, 400, 625],
    )
    const subscribeField = createCheckboxField(
    'subscribe',
    page.reference,
    [200, 575, 215, 590],
    )

    document.add(nameField)
    document.add(emailField)
    document.add(phoneField)
    document.add(subscribeField)

    // Add annotations to page
    page.content.set(
    'Annots',
    new PdfArray([
    nameField.reference,
    emailField.reference,
    phoneField.reference,
    subscribeField.reference,
    ]),
    )

    // Create pages collection
    const pages = createPages([page])
    page.content.set('Parent', pages.reference)
    document.add(pages)

    // Create catalog
    const catalog = createCatalog(pages.reference)

    // Create AcroForm with all fields
    const acroForm = new PdfDictionary()
    acroForm.set(
    'Fields',
    new PdfArray([
    nameField.reference,
    emailField.reference,
    phoneField.reference,
    subscribeField.reference,
    ]),
    )
    // NeedAppearances flag tells PDF readers to generate appearance streams
    acroForm.set('NeedAppearances', new PdfBoolean(true))

    // Default resources for the form (font)
    const formResources = new PdfDictionary()
    const formFontDict = new PdfDictionary()
    const helveticaFont = new PdfDictionary()
    helveticaFont.set('Type', new PdfName('Font'))
    helveticaFont.set('Subtype', new PdfName('Type1'))
    helveticaFont.set('BaseFont', new PdfName('Helvetica'))
    formFontDict.set('Helv', helveticaFont)
    formResources.set('Font', formFontDict)
    acroForm.set('DR', formResources)

    const acroFormObj = new PdfIndirectObject({ content: acroForm })
    document.add(acroFormObj)
    catalog.content.set('AcroForm', acroFormObj.reference)

    document.add(catalog)

    // Set the catalog as the root
    document.trailerDict.set('Root', catalog.reference)

    await document.commit()

    // Save the empty form
    // This demonstrates creating a blank form that users can fill in
    await fs.writeFile(`${tmpFolder}/form-empty.pdf`, document.toBytes())
    console.log('Created form-empty.pdf with empty form fields')

    // ============================================
    // PART 2: Fill in the form fields
    // ============================================
    // This demonstrates how to programmatically fill in form fields.
    // We now read the previously saved empty form and modify it.

    // Read the empty form PDF
    const emptyFormBytes = await fs.readFile(`${tmpFolder}/form-empty.pdf`)
    const filledDocument = await PdfDocument.fromBytes([emptyFormBytes])

    // Get the catalog reference from trailer
    const catalogRef = filledDocument.trailerDict.get('Root')
    if (!catalogRef || !(catalogRef instanceof PdfObjectReference)) {
    throw new Error('No catalog found in PDF')
    }

    // Read the catalog object
    const catalogObj = await filledDocument.readObject({
    objectNumber: catalogRef.objectNumber,
    })
    if (!catalogObj || !(catalogObj.content instanceof PdfDictionary)) {
    throw new Error('Catalog object not found')
    }

    // Get the AcroForm reference
    const acroFormRef = catalogObj.content.get('AcroForm')
    if (!acroFormRef || !(acroFormRef instanceof PdfObjectReference)) {
    throw new Error('No AcroForm found in PDF')
    }

    // Read the AcroForm object
    const filledAcroFormObj = await filledDocument.readObject({
    objectNumber: acroFormRef.objectNumber,
    })
    if (
    !filledAcroFormObj ||
    !(filledAcroFormObj.content instanceof PdfDictionary)
    ) {
    throw new Error('AcroForm object not found')
    }

    // Get the fields array
    const fieldsArray = filledAcroFormObj.content.get('Fields')
    if (!fieldsArray || !(fieldsArray instanceof PdfArray)) {
    throw new Error('No fields found in AcroForm')
    }

    // Helper function to find a field by name
    async function findField(
    fieldName: string,
    ): Promise<PdfIndirectObject<PdfDictionary> | null> {
    for (const fieldRef of fieldsArray.items) {
    if (!(fieldRef instanceof PdfObjectReference)) continue
    const fieldObj = await filledDocument.readObject({
    objectNumber: fieldRef.objectNumber,
    })
    if (!fieldObj || !(fieldObj.content instanceof PdfDictionary)) continue

    const name = fieldObj.content.get('T')
    if (name instanceof PdfString) {
    // Convert bytes to string for comparison
    const nameStr = name.value
    if (nameStr === fieldName) {
    return fieldObj as PdfIndirectObject<PdfDictionary>
    }
    }
    }
    return null
    }

    // Update the name field value
    const nameFieldObj = await findField('name')
    if (nameFieldObj) {
    nameFieldObj.content.set('V', new PdfString('John Doe'))
    }

    // Update the email field value
    const emailFieldObj = await findField('email')
    if (emailFieldObj) {
    emailFieldObj.content.set('V', new PdfString('john.doe@example.com'))
    }

    // Update the phone field value
    const phoneFieldObj = await findField('phone')
    if (phoneFieldObj) {
    phoneFieldObj.content.set('V', new PdfString('+1 (555) 123-4567'))
    }

    // Check the subscribe checkbox
    const subscribeFieldObj = await findField('subscribe')
    if (subscribeFieldObj) {
    subscribeFieldObj.content.set('V', new PdfName('Yes'))
    subscribeFieldObj.content.set('AS', new PdfName('Yes'))
    }

    // Save the filled form
    await fs.writeFile(`${tmpFolder}/form-filled.pdf`, filledDocument.toBytes())
    console.log('Created form-filled.pdf with filled form fields')

    console.log('\nForm field values:')
    console.log('- Name: John Doe')
    console.log('- Email: john.doe@example.com')
    console.log('- Phone: +1 (555) 123-4567')
    console.log('- Subscribe: Yes')
    import { PdfByteStreamTokeniser } from 'pdf-lite/core/tokeniser'
    import { PdfToken } from 'pdf-lite/core/tokens/token'
    import { stringToBytes } from 'pdf-lite/utils/stringToBytes'

    /**
    * This example demonstrates how to use the PdfByteStreamTokeniser
    * to tokenize PDF content into individual tokens.
    *
    * The tokeniser converts raw PDF bytes into a stream of tokens that can
    * be further processed by the decoder or used for PDF analysis.
    */

    // Sample PDF content to tokenize
    const pdfContent = `%PDF-2.0
    1 0 obj
    << /Type /Catalog /Pages 2 0 R >>
    endobj
    2 0 obj
    << /Type /Pages /Kids [3 0 R] /Count 1 >>
    endobj
    3 0 obj
    << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>
    endobj
    trailer
    << /Size 4 /Root 1 0 R >>
    startxref
    0
    %%EOF`

    // Create the tokeniser
    const tokeniser = new PdfByteStreamTokeniser()

    // Convert the PDF content to bytes and feed it to the tokeniser
    const bytes = stringToBytes(pdfContent)
    tokeniser.feedBytes(bytes)

    // Signal end of input
    tokeniser.eof = true

    // Collect all tokens
    const tokens: PdfToken[] = []
    for (const token of tokeniser.nextItems()) {
    tokens.push(token)
    }

    // Display tokenisation results
    console.log('PDF Tokenisation Example')
    console.log('========================\n')
    console.log(`Input: ${pdfContent.length} bytes`)
    console.log(`Output: ${tokens.length} tokens\n`)

    // Group tokens by type for summary
    const tokenCounts = new Map<string, number>()
    for (const token of tokens) {
    const type = token.constructor.name
    tokenCounts.set(type, (tokenCounts.get(type) ?? 0) + 1)
    }

    console.log('Token type counts:')
    for (const [type, count] of tokenCounts) {
    console.log(` ${type}: ${count}`)
    }

    console.log('\nFirst 20 tokens:')
    for (const token of tokens.slice(0, 20)) {
    const tokenString = token.toString().slice(0, 40)
    const displayString =
    tokenString.length >= 40 ? tokenString + '...' : tokenString
    console.log(
    ` ${token.constructor.name.padEnd(30)} ${JSON.stringify(displayString)}`,
    )
    }

    // Example: Tokenising incrementally (useful for streaming)
    console.log('\n\nIncremental Tokenisation Example')
    console.log('=================================\n')

    const incrementalTokeniser = new PdfByteStreamTokeniser()

    // Feed bytes in chunks (simulating streaming)
    const chunkSize = 50
    const numChunks = Math.ceil(bytes.length / chunkSize)

    console.log(`Processing ${numChunks} chunks of ~${chunkSize} bytes each...`)

    let totalTokens = 0
    for (let i = 0; i < bytes.length; i += chunkSize) {
    const chunk = bytes.slice(i, i + chunkSize)
    incrementalTokeniser.feedBytes(chunk)

    // Process tokens as they become available
    for (const _ of incrementalTokeniser.nextItems()) {
    totalTokens++
    }
    }

    // Signal end of input and collect remaining tokens
    incrementalTokeniser.eof = true
    for (const _ of incrementalTokeniser.nextItems()) {
    totalTokens++
    }

    console.log(`Total tokens produced: ${totalTokens}`)

    // Example: Custom stream chunk size
    console.log('\n\nCustom Stream Chunk Size Example')
    console.log('================================\n')

    const customTokeniser = new PdfByteStreamTokeniser({
    streamChunkSizeBytes: 512, // Customize the chunk size for stream content
    })

    const streamContent = `1 0 obj
    << /Length 100 >>
    stream
    This is stream content that will be chunked by the tokeniser.
    The chunk size determines how the stream data is delivered.
    endstream
    endobj`

    customTokeniser.feedBytes(stringToBytes(streamContent))
    customTokeniser.eof = true

    console.log('Tokens from stream content:')
    for (const token of customTokeniser.nextItems()) {
    const type = token.constructor.name
    const preview = token.toString().slice(0, 50).replace(/\n/g, '\\n')
    console.log(` ${type.padEnd(25)} ${JSON.stringify(preview)}`)
    }
    import { PdfDecoder } from 'pdf-lite/core/decoder'
    import { PdfByteStreamTokeniser } from 'pdf-lite/core/tokeniser'
    import { pdfDecoder } from 'pdf-lite/core/generators'
    import { PdfObject } from 'pdf-lite/core/objects/pdf-object'
    import { PdfIndirectObject } from 'pdf-lite/core/objects/pdf-indirect-object'
    import { PdfDictionary } from 'pdf-lite/core/objects/pdf-dictionary'
    import { PdfStream } from 'pdf-lite/core/objects/pdf-stream'
    import { PdfTrailer } from 'pdf-lite/core/objects/pdf-trailer'
    import { PdfXRefTable } from 'pdf-lite/core/objects/pdf-xref-table'
    import { PdfComment } from 'pdf-lite/core/objects/pdf-comment'
    import { PdfStartXRef } from 'pdf-lite/core/objects/pdf-start-xref'
    import { stringToBytes } from 'pdf-lite/utils/stringToBytes'

    /**
    * This example demonstrates how to use the PdfDecoder
    * to decode PDF tokens into PDF objects.
    *
    * The decoder transforms a stream of tokens (from the tokeniser)
    * into high-level PDF objects like dictionaries, arrays, streams,
    * and indirect objects.
    */

    // Sample PDF content to decode
    const pdfContent = `%PDF-2.0
    1 0 obj
    << /Type /Catalog /Pages 2 0 R >>
    endobj
    2 0 obj
    << /Type /Pages /Kids [3 0 R] /Count 1 >>
    endobj
    3 0 obj
    << /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R >>
    endobj
    4 0 obj
    << /Length 44 >>
    stream
    BT /F1 12 Tf 100 700 Td (Hello!) Tj ET
    endstream
    endobj
    xref
    0 5
    0000000000 65535 f
    0000000010 00000 n
    0000000060 00000 n
    0000000118 00000 n
    0000000217 00000 n
    trailer
    << /Size 5 /Root 1 0 R >>
    startxref
    310
    %%EOF`

    // Method 1: Using the pdfDecoder generator (recommended)
    console.log('PDF Decoder Example - Using pdfDecoder Generator')
    console.log('================================================\n')

    const bytes = stringToBytes(pdfContent)
    const objects: PdfObject[] = []

    for (const obj of pdfDecoder([bytes])) {
    objects.push(obj)
    }

    console.log(`Decoded ${objects.length} PDF objects:\n`)

    for (const obj of objects) {
    const type = obj.constructor.name

    if (obj instanceof PdfComment) {
    console.log(` ${type}: ${obj.toString().trim()}`)
    } else if (obj instanceof PdfIndirectObject) {
    const contentType = obj.content.constructor.name
    console.log(
    ` ${type}: ${obj.objectNumber} ${obj.generationNumber} obj (${contentType})`,
    )

    // Show dictionary keys if content is a dictionary or stream
    if (obj.content instanceof PdfDictionary) {
    const keys = Object.keys(obj.content.values)
    console.log(` Keys: ${keys.join(', ')}`)
    }
    if (obj.content instanceof PdfStream) {
    console.log(
    ` Stream length: ${obj.content.original.length} bytes`,
    )
    }
    } else if (obj instanceof PdfXRefTable) {
    console.log(` ${type}: ${obj.entries.length} entries`)
    } else if (obj instanceof PdfTrailer) {
    const size = obj.dict.get('Size')?.toString() ?? 'unknown'
    console.log(` ${type}: Size=${size}`)
    } else if (obj instanceof PdfStartXRef) {
    console.log(` ${type}: offset=${obj.offset}`)
    } else {
    console.log(` ${type}`)
    }
    }

    // Method 2: Using the PdfDecoder class directly with a tokeniser
    console.log('\n\nPDF Decoder Example - Manual Pipeline')
    console.log('======================================\n')

    const tokeniser = new PdfByteStreamTokeniser()
    const decoder = new PdfDecoder({ ignoreWhitespace: true })

    // Feed bytes to tokeniser
    tokeniser.feedBytes(bytes)
    tokeniser.eof = true

    // Feed tokens to decoder
    for (const token of tokeniser.nextItems()) {
    decoder.feed(token)
    }
    decoder.eof = true

    // Collect decoded objects
    const manualObjects: PdfObject[] = []
    for (const obj of decoder.nextItems()) {
    manualObjects.push(obj)
    }

    console.log(`Decoded ${manualObjects.length} objects with whitespace ignored\n`)

    // Count objects by type
    const typeCounts = new Map<string, number>()
    for (const obj of manualObjects) {
    const type = obj.constructor.name
    typeCounts.set(type, (typeCounts.get(type) ?? 0) + 1)
    }

    console.log('Object type counts:')
    for (const [type, count] of typeCounts) {
    console.log(` ${type}: ${count}`)
    }

    // Example: Incremental decoding (useful for streaming)
    console.log('\n\nIncremental Decoding Example')
    console.log('============================\n')

    const streamTokeniser = new PdfByteStreamTokeniser()
    const streamDecoder = new PdfDecoder()

    // Simulate streaming by processing in chunks
    const chunkSize = 100
    let objectCount = 0

    for (let i = 0; i < bytes.length; i += chunkSize) {
    const chunk = bytes.slice(i, i + chunkSize)
    streamTokeniser.feedBytes(chunk)

    // Process available tokens
    for (const token of streamTokeniser.nextItems()) {
    streamDecoder.feed(token)

    // Collect any complete objects
    for (const obj of streamDecoder.nextItems()) {
    objectCount++
    console.log(
    ` Chunk ${Math.floor(i / chunkSize) + 1}: Found ${obj.constructor.name}`,
    )
    }
    }
    }

    // Finalize
    streamTokeniser.eof = true
    streamDecoder.eof = true

    for (const token of streamTokeniser.nextItems()) {
    streamDecoder.feed(token)
    }

    for (const obj of streamDecoder.nextItems()) {
    objectCount++
    console.log(` Final: Found ${obj.constructor.name}`)
    }

    console.log(`\nTotal objects decoded incrementally: ${objectCount}`)

    // Example: Preserving whitespace for round-trip
    console.log('\n\nRound-Trip Example (Preserving Whitespace)')
    console.log('==========================================\n')

    const simpleDict = `<< /Type /Page /MediaBox [0 0 612 792] >>`
    const preservingDecoder = pdfDecoder([stringToBytes(simpleDict)], {
    ignoreWhitespace: false,
    })

    for (const obj of preservingDecoder) {
    // toString() will recreate the original representation
    const reconstructed = obj.toString()
    console.log('Original: ', JSON.stringify(simpleDict))
    console.log('Reconstructed:', JSON.stringify(reconstructed))
    console.log('Match:', simpleDict === reconstructed)
    }