pdfAnnotate

repository·master·Indexed 20 days ago

https://github.com/highkite/pdfannotate

A JavaScript library (version 1.0.15) for creating annotations in PDF documents across browser and Node.js environments. It provides an API to manipulate PDF objects and supports various annotation types including Text, Highlight, Underline, Squiggly, StrikeOut, and FreeText. Designed to work alongside renderers like PDF.js, it includes features for coordinate translation, quadpoints, and appearance streams to ensure consistent rendering across different PDF viewers.

Tokens
12.6K
Snippets
47
Records
59
Agent score
71%

What's inside annotpdf

  1. How PDF Objects and References work

    master

    A PDF document is composed of various objects (Page objects, Root objects, Content objects, etc.).

    Object Identification: Objects are identified by an object ID and a generation.

    • Object ID: A unique number for the object.
    • Generation: A value used to differentiate between different objects that happen to reuse the same ID (after an old object was freed). The generation increments every time an ID is reused.

    Referencing Objects: To reference an object, use the format [object ID] [generation] R. For example, to reference an object with ID 3 and generation 0, you write 3 0 R.

    3 0 obj
    ...
    endobj
    
    // Reference example:
    3 0 R
  2. Decompressing Stream Objects

    master

    Stream compression is controlled by the /Filter and /DecodeParms attributes.

    • /Filter: Specifies the compression algorithm. Currently, only Flate decoding is supported.
    • /DecodeParms: Optional parameters for additional filtering (e.g., PNG predictor functions). If predictors are used, they must be inverted during uncompression.
  3. How pdfAnnotate works

    master

    Unlike many PDF viewer libraries (such as pdfjs) that only support viewing, pdfAnnotate is designed to support the creation of annotations that can be written directly back into the PDF file.

    Instead of using a high-level PDF writer library (like pdfkit, pdfmake, or jspdf) to reconstruct the entire document—which can lead to issues with font parsing (e.g., CFF vs TTF) and complex data structures—pdfAnnotate utilizes the Updating section of the PDF specification. This allows the library to add, edit, or delete annotations by appending them to the end of the existing PDF file, ensuring the integrity of the original document while making annotations a permanent part of the file itself.

  4. Use the dictionary pattern for annotation creation

    master

    For better compatibility and to manage the large number of available parameters, pdfAnnotate uses a dictionary pattern. While older versions allowed positional arguments, you should pass a single configuration object as the last parameter to creation functions.

    Recommended Pattern:

    factory.createTextAnnotation({
        page: 0,
        rect: [50, 50, 80, 80],
        contents: "Pop up note",
        author: "Max"
    });
    AnnotationFactory.loadFile(path).then((factory) => {
        factory.createTextAnnotation({
            page: 0,
            rect: [50, 50, 80, 80],
            contents: "Pop up note",
            author: "Max"
        })
        factory.download()
    })
  5. Understanding Cross-Reference Stream Objects

    master

    Introduced in PDF 1.5, cross-reference stream objects allow for compressed reference information. These are regular PDF stream objects with a /Type of /XRef.

    Key Attributes:

    • /W: Determines the length of a cross-reference entry. For example, /W [1 3 1] means the entry is 5 bytes total (1 byte for type, 3 bytes for pointer, 1 byte for generation).
    • /Index: Defines the range of object IDs covered by the stream. If not present, the stream is treated as starting from object ID 0.
    • /Root: Points to the previous cross-reference stream object.

    Entry Types (determined by the first byte of the entry):

    • Type 0 (Freed Objects): Encodes a linked list of freed object IDs. The first value is the ID of the next freed object, and the second is the generation to use when reusing it.
    • Type 1 (Regular Objects): Encodes the byte offset/pointer to the PDF object.
    • Type 2 (Compressed Objects): Used when objects themselves are compressed within a stream.
    • Type 3 (Stream Objects): Used in stream objects to point to the object ID and an offset within that stream.
    3680 0 obj <<
    /Type /XRef
    /Index [0 3681]
    /Size 3681
    /W [1 3 1]
    /Root 3678 0 R
    /Info 3679 0 R
    /ID [<4E4CF7709370170501AFF281926C390D> <4E4CF7709370170501AFF281926C390D>]
    /Length 8605
    /Filter /FlateDecode
    >>
    stream
    ...
    endstream
    endobj
  6. Understanding PDF Document Updates

    master

    When a PDF is modified (e.g., adding an annotation), a new triplet is appended to the end of the file to avoid rewriting the entire document. This triplet consists of:

    1. Body update: Contains the new or updated objects (e.g., the new annotation and the updated page annotation list).
    2. Cross-reference section (xref): Contains jump addresses for the updated objects. It is divided into subsections. Each subsection starts with two numbers: the object ID of the first object in the subsection and the number of references in that subsection.
    3. Updated Trailer: Contains the trailer dictionary, which includes:
      • Root: Points to the catalog dictionary.
      • Prev: Points to the previous cross-reference section (absent in the first trailer).
      • Size: The total number of entries in the complete cross-reference table history.
    4. startxref: The byte position where the xref keyword begins.

    Subsection Entry Structure: Each entry contains a pointer to the object location (byte offset) and a generation number (used to track reused object IDs). Entries are marked with n (new/updated object) or f (freed object).

    7 0 obj [6 0 R 8 0 R ] endobj
    8 0 obj <</Type /Annot /Rect [77.7777777778 83.7931904161 83.7777777778 89.7856242119 ] /Subtype /Text /M (D:20190101154225) /T (max) /Contents (Pop up note) /NM (okular) /F 4 /C [1 1 0 ] /CA 1 /Border [0 0 1 ] /P 3 0 R >> endobj
    xref
    0 1
    0000000001 65535 f
    7 2
    0000001321 00000 n
    0000001352 00000 n
    trailer
    <</Size 9 /Root 1 0 R /Prev 1150 >>
    startxref
    1648
    %%EOF
  7. How adding an annotation works

    master

    Adding an annotation to a PDF involves updating the document structure by appending new objects and updating the cross-reference table.

    High-level procedure:

    1. Create the annotation object: Generate a new annotation object using a free object ID (either by reusing a freed ID or incrementing the last used ID).
    2. Update the Page object: Fetch the latest Page object. If it lacks an Annots field, you must create one. This field holds a list of references to all annotations on that page.
    3. Append objects: Append the new annotation object and, if necessary, the new Annots list object to the PDF document.
    4. Update Cross-Reference (xref) table: Create and attach a new cross-reference section to the file data to ensure the new objects are reachable.
    5. Finalize: Process or download the modified file.

    Requirements for creation:

    • A free object ID (new or reused).
    • The address of the existing Annots field or the Page object (if creating a new list).
    • The last update section to correctly handle references and the cross-reference table.
  8. How Stream Objects work

    master

    A stream object with /Type /ObjStm contains multiple encoded objects.

    Key Attributes:

    • /N: The number of encoded objects.
    • /First: The starting position of the first object within the stream.
    • /Length: The length of the stream.
    • /Filter: The compression algorithm used (e.g., /FlateDecode).

    Structure:

    1. Cross-reference part: The first part of the stream acts as a table for objects within the stream. Each entry consists of an object ID and an offset. The actual data for object $i$ is located at /First + offset_i.
    2. Object data part: The second part contains the actual object data.

    Important Constraints:

    • All generation values for objects within a stream object are 0. Objects with reused IDs cannot be compressed into stream objects.
    • The obj and endobj keywords are omitted within the stream.
    3488 0 obj <<
    /Type /ObjStm
    /N 100
    /First 1021
    /Length 6246
    /Filter /FlateDecode
    >>
    stream
    ...
    endstream
    endobj
  9. Understand Quadpoints in PDF annotations

    master

    Quadpoints allow you to define annotations using multiple rectangles, where each rectangle is defined by four points (x and y coordinates).

    Key rules for Quadpoints:

    • Array Length: The length of the quadpoints array must be a multiple of 8.
    • Coordinate Order: The library uses a specific order for the four points (refer to the documentation image for visual confirmation).
    • Fallback Behavior: If quadpoints are not provided, the API derives them from the rect array. However, providing them explicitly allows for more complex shapes.
    • Bounding Box: If provided quadpoints fall outside the bounding box specified by the Rect array, conforming readers may ignore the QuadPoints array.
  10. Translate UI coordinates to PDF coordinates

    master

    When implementing a web-based editor, mouse click coordinates from the browser (e.g., pageX, pageY) do not match the PDF coordinate system due to offsets and scaling.

    To correctly position annotations, you must:

    1. Subtract the element's offset from the click coordinates.
    2. Use the PDF.js viewport method convertToPdfPoint(x, y) to translate the scaled viewport coordinates into the PDF's internal coordinate system.

    Note: The PDF coordinate system origin is the bottom-left corner of the page.

    pdfContainer.addEventListener('click', (evt) => {
        // 1. Calculate offset relative to the page element
        let x = evt.pageX - $('#page' + pdfViewer.currentPageNumber).offset().left;
        let y = evt.pageY - $('#page' + pdfViewer.currentPageNumber).offset().top;
    
        // 2. Convert viewport coordinates to PDF points
        // This accounts for scaling and coordinate system shifts
        let pdfPoint = pdfViewer._pages[pdfViewer.currentPageNumber - 1].viewport.convertToPdfPoint(x, y);
        // pdfPoint now contains the correct [x, y] for the annotation
    });
  11. Get started with pdfAnnotate

    master

    To use the library, you must initialize an AnnotationFactory. There are two primary ways to do this:

    1. Using a file path: Use the static loadFile method to load a PDF from a specific path.
    2. Using existing PDF.js data: If you are already using a renderer like PDF.js, you can pass the raw data from the pdfDocument directly into the AnnotationFactory constructor.

    Once initialized, you can use various creator methods to add annotations and finally call download() to save the annotated document.

    // Approach 1: Loading from a file path
    import {AnnotationFactory} from 'annotpdf';
    
    AnnotationFactory.loadFile(path).then((factory) => {
        factory.createTextAnnotation({
            page: 0,
            rect: [50, 50, 80, 80],
            contents: "Pop up note",
            author: "Max"
        });
        factory.download();
    });
    
    // Approach 2: Initializing with PDF.js data
    // Assuming pdfDocument is a PDFjs document from pdfjsLib.getDocument(...)
    pdfDocument.getData().then((data) => {
        let pdfFactory = new AnnotationFactory(data);
    });
  12. Enable Appearance Stream support for consistent rendering

    master

    Since version 1.0.15, the library supports appearance streams. This ensures annotations look the same across different PDF renderers (like pdfjs) and allows rendering annotations that might not be natively supported by the viewer (e.g., freetext in pdfjs).

    To use the library's default appearance stream, call createDefaultAppearanceStream() on the returned annotation object.

    let ta = factory.createPolygonAnnotation(val)
    ta.createDefaultAppearanceStream()