Skip to content

putImage mutates the global filters array, causing uncompressed output on repeated save()/output() when document has images #3986

Description

@douglasmatheus

I have read and understood the contribution guidelines.

Version: 4.2.1 (also confirmed in 3.0.4)

Summary

Calling doc.output() (or doc.save()) more than once on a jsPDF document that contains any image produces PDFs of dramatically different sizes. The first output is correct; every subsequent output emits uncompressed page content streams. The PDF still renders fine, but its size balloons.

Minimal repro

const doc = new jsPDF({ compress: true });

// Any image triggers the bug
const jpg = "data:image/jpeg;base64,/9j/4AAQSk..."; // any base64 JPEG
doc.addImage(jpg, "JPEG", 10, 10, 100, 75);

const a = doc.output();
const b = doc.output();
const c = doc.output();

console.log(a.length, b.length, c.length);
// Observed: 5440, 110023, 110023  (b and c emit page stream uncompressed)
// Expected: a === b === c

The size jump only happens with images (or anything else that goes through addImage - LaTeX, watermarks via canvas, etc.). Documents that contain only text produce identical outputs across calls.

Diagnosis

getFilters() (src/jspdf.js:1748) returns the live reference to the document's internal filters array:

var getFilters = (API.__private__.getFilters = function() {
  return filters;
});

putImage (src/modules/addimage.js:206) calls splice() on that reference to remove "FlateEncode" before writing image data (which is already compressed, so re-applying Flate would be wasted work):

var filter = getFilters();
while (filter.indexOf("FlateEncode") !== -1) {
  filter.splice(filter.indexOf("FlateEncode"), 1);
}

Since filter is the document's filters array, the splice permanently strips "FlateEncode" from the global configuration. The next call to putStream (e.g. for a page content stream during the second output()) sees an empty filter chain and writes the page raw.

Inspecting the output confirms the cause

In the first PDF, the page content stream object has /Filter /FlateDecode and is ~5 KB. In the second PDF, the same object has no /Filter and is ~95 KB - the joined pages[1] operators emitted verbatim:

4 0 obj
<<
/Length 95357
>>
stream
0.75 w
0. G
q
1. 1. 1. rg
...

Fix

Trivial - clone the array before mutating:

var filter = getFilters().slice();

I have a PR ready with this change plus a regression test: #3987 .

Workarounds (for anyone hitting this before a release):

  1. After each output()/save(), push "FlateEncode" back into pdf.internal.getFilters() if it's missing.
  2. Monkey-patch pdf.internal.getFilters to return .slice().
  3. Create a fresh jsPDF instance per export.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions