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):
- After each
output()/save(), push "FlateEncode" back into pdf.internal.getFilters() if it's missing.
- Monkey-patch
pdf.internal.getFilters to return .slice().
- Create a fresh
jsPDF instance per export.
I have read and understood the contribution guidelines.
Version: 4.2.1 (also confirmed in 3.0.4)
Summary
Calling
doc.output()(ordoc.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
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 internalfiltersarray:putImage(src/modules/addimage.js:206) callssplice()on that reference to remove"FlateEncode"before writing image data (which is already compressed, so re-applying Flate would be wasted work):Since
filteris the document'sfiltersarray, the splice permanently strips"FlateEncode"from the global configuration. The next call toputStream(e.g. for a page content stream during the secondoutput()) 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 /FlateDecodeand is ~5 KB. In the second PDF, the same object has no/Filterand is ~95 KB - the joinedpages[1]operators emitted verbatim:Fix
Trivial - clone the array before mutating:
I have a PR ready with this change plus a regression test: #3987 .
Workarounds (for anyone hitting this before a release):
output()/save(), push"FlateEncode"back intopdf.internal.getFilters()if it's missing.pdf.internal.getFiltersto return.slice().jsPDFinstance per export.