Skip to content

Commit

Permalink
Performance improvements
Browse files Browse the repository at this point in the history
Annotate will not attempt to load all PDF pages before opening the file.

Also add some documentation.
  • Loading branch information
Semphriss committed Jun 4, 2024
1 parent b4ea53a commit 91da24c
Show file tree
Hide file tree
Showing 3 changed files with 141 additions and 33 deletions.
23 changes: 16 additions & 7 deletions www/js/document.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -64,11 +64,11 @@ export class Document {
// Special case: freshly imported files have no pages; generate them
if (dataPages.filter(p => p.length !== 0).length === 0 && that.pdf) {
for (let i = 1; i <= that.pdf.getNumPages(); i++) {
that.addPage(DocumentPage.fromPdfPage(await that.pdf.getPage(i)));
that.addPage(DocumentPage.fromPdfPage(that.pdf, i, container));
}
} else {
for (const dataPage of dataPages.filter(p => p.length !== 0)) {
that.addPage(await DocumentPage.fromSaveData(dataPage, that.pdf));
that.addPage(DocumentPage.fromSaveData(dataPage, that.pdf, container));
}
}

Expand All @@ -86,7 +86,7 @@ export class Document {
that.name = name;

for (let i = 1; i <= that.pdf.getNumPages(); i++) {
that.addPage(DocumentPage.fromPdfPage(await that.pdf.getPage(i)));
that.addPage(DocumentPage.fromPdfPage(that.pdf, i, container));
}

return that;
Expand All @@ -101,7 +101,7 @@ export class Document {
that.pdf = null;
that.container = container;
that.name = name;
that.addPage(DocumentPage.fromEmpty());
that.addPage(DocumentPage.fromEmpty(container));

return that;
}
Expand All @@ -110,9 +110,6 @@ export class Document {
* Adds a page to the document at the specified position.
*/
addPage(page, position = -1) {
this.container.appendChild(page.getCanvas());
page.refresh();

if (position < 0) {
this.pages.push(page);
} else {
Expand Down Expand Up @@ -154,16 +151,28 @@ export class Document {
await saveFile(this.name, serialized);
}

/**
* Call cb each time the document is saved. The first and only parameter is
* true if there will be another invocation soon (the document will save
* again) and false otherwise.
*/
onSaved(cb) {
this.saveCbs.push(cb);
}

/**
* Call the callbacks supplied by onSaved(). This function should be
* considered private.
*/
notifyOnSaved(needsRerun) {
for (const cb of this.saveCbs) {
cb(needsRerun);
}
}

/**
* Export the document to a PDF document.
*/
async exportPdf() {
const pdfDoc = await PDFDocument.create();
const originalPdf = (!this.pdf) ? null
Expand Down
105 changes: 81 additions & 24 deletions www/js/document_page.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,16 @@ import { StrokeElement } from './elements/stroke.mjs';
*/
export class DocumentPage {
/* PdfPage? */ pdfPage = null;
/* index */ index = -1;
/* HTMLCanvasElement */ canvas = document.createElement('canvas');
/* { width, height } */ baseDims = {};
/* Element[] */ elements = [];
/* Element? */ tempElement = null;

static async fromSaveData(data, pdf) {
/**
* Load a document page from save data.
*/
static fromSaveData(data, pdf, container) {
const d = data.split('\t');

if (d.length !== 4) {
Expand All @@ -38,13 +42,13 @@ export class DocumentPage {

// index might be NaN in case of malformed document
if (index >= 0) {
doc = DocumentPage.fromPdfPage(await pdf.getPage(index));
doc = DocumentPage.fromPdfPage(pdf, index, container);
d.shift();
d.shift();
} else {
let width = parseInt(d.shift());
let height = parseInt(d.shift());
doc = DocumentPage.fromEmpty({ width, height });
doc = DocumentPage.fromEmpty(container, { width, height });
}

for (const elementData of d.shift().split(';').filter(e => e.length)) {
Expand All @@ -58,35 +62,58 @@ export class DocumentPage {
return doc;
}

static fromPdfPage(pdfPage) {
/**
* Create a Document page from a page in a PDF file.
*/
static fromPdfPage(pdf, index, container) {
const that = new DocumentPage();

that.pdfPage = pdfPage;
that.pdfPage.onRenderReady((() => { that.draw(); }));
const viewport = that.pdfPage.getDims();
that.baseDims.width = viewport.width;
that.baseDims.height = viewport.height;
that.init();
container.appendChild(that.canvas);

// Temporary default values (before the PDF page data is fetched)
that.baseDims.width = 1920;
that.baseDims.height = 1080;
that.adjustSize();

that.index = index;

// Function not async so that the caller can get a valid DocumentPage
// without waiting for it to load
(async () => {
that.pdfPage = await pdf.getPage(index);

const viewport = that.pdfPage.getBaseDims();
that.baseDims.width = viewport.width;
that.baseDims.height = viewport.height;

that.pdfPage.onRenderReady((() => {
that.draw();
}));

that.refresh();
})();

ToolHandler.bindPage(that);

return that;
}

static fromEmpty(dims = { width: 1920, height: 1080 }) {
/**
* Create a new, empty Document page.
*/
static fromEmpty(container, dims = { width: 1920, height: 1080 }) {
const that = new DocumentPage();

container.appendChild(that.canvas);

that.pdfPage = null;
that.baseDims.width = dims.width;
that.baseDims.height = dims.height;
that.init();
that.adjustSize();

return that;
}

init() {
this.canvas.width = this.baseDims.width;
this.canvas.height = this.baseDims.height;
ToolHandler.bindPage(that);

ToolHandler.bindPage(this);
return that;
}

/**
Expand Down Expand Up @@ -119,6 +146,10 @@ export class DocumentPage {
return this.canvas;
}

/**
* Draw the current page on its canvas or, optionally, using a different ctx
* supplied in parameter.
*/
draw(ctx = this.canvas.getContext('2d')) {
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);

Expand All @@ -135,12 +166,24 @@ export class DocumentPage {
}
}

refresh() {
/**
* Resize the canvas dimensions to fit the page. This function does not
* invoke any redrawing mechanism; usage of refresh() is preferred.
*/
adjustSize() {
const width = this.canvas.clientWidth * window.devicePixelRatio;
const baseDims = this.getBaseDims();

this.canvas.width = width;
this.canvas.height = width / baseDims.width * baseDims.height;
}

/**
* Resize the canvas dimensions to fit the page, and redraws the page with
* the new dimensions.
*/
refresh() {
this.adjustSize();

if (this.pdfPage) {
this.pdfPage.autoscale(this.canvas.width);
Expand All @@ -150,19 +193,30 @@ export class DocumentPage {
}
}

/**
* Add the given element to the page.
*/
addElement(element) {
this.elements.push(element);
}

/**
* Put an element on the page, but don't serialize it on save/export. There
* can be at most one temporary element. It is meant to be used by tools.
*/
setTempElement(element) {
this.tempElement = element;
}

/**
* Convert the data in this page into a string that can be passed to
* fromSaveData().
*/
serialize() {
var data = "";

if (this.pdfPage) {
data += this.pdfPage.index + "\t\t\t";
if (this.index >= 0) {
data += this.index + "\t\t\t";
} else {
data += `-1\t${this.baseDims.width}\t${this.baseDims.height}\t`;
}
Expand All @@ -174,11 +228,14 @@ export class DocumentPage {
return data;
}

/**
* Adds the corrent page to the PDF-LIB document.
*/
async exportPdf(pdf, originalPdf) {
let currPage;

if (this.pdfPage && originalPdf) {
const [copy] = await pdf.copyPages(originalPdf, [this.pdfPage.index - 1]);
if (this.index >= 0 && originalPdf) {
const [copy] = await pdf.copyPages(originalPdf, [this.index - 1]);
currPage = pdf.addPage(copy);
} else {
const dims = this.getBaseDims();
Expand Down
46 changes: 44 additions & 2 deletions www/js/pdf_page.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -23,23 +23,33 @@ export class PdfPage {
/* number */ index;
/* HTMLCanvasElement */ canvas = document.createElement('canvas');
/* number */ baseWidth = 0;
/* number */ baseHeight = 0;
/* number */ currentWidth = 0;
/* Job */ renderJob = new Job(this.render.bind(this),
this.notifyRenderReady.bind(this));
/* function[] */ cbs = [];
/* bool */ hasRenderedOnce = false;

/**
* Create a new PdfPage with the supplied Pdf document and page index.
*/
static async fromPdf(pdf, index) {
const that = new PdfPage();

that.page = await pdf.getPage(index);
that.index = index;
that.baseWidth = that.page.getViewport({ scale: 1 }).width;
that.autoscale(that.baseWidth);

const baseDims = that.page.getViewport({ scale: 1 });
that.baseWidth = baseDims.width;
that.baseHeight = baseDims.height;

return that;
}

/**
* Pre-render the PDF page on a canvas, which will hold the resulting image
* for faster redraws.
*/
async render() {
const id = Math.random();

Expand All @@ -60,11 +70,17 @@ export class PdfPage {
this.hasRenderedOnce = true;
}

/**
* Adjust the scale of the PdfPage to accomodate for the new width.
*/
autoscale(width) {
this.currentWidth = width;
this.renderJob.run();
}

/**
* Draw the cached PDF page on the canvas,
*/
draw(ctx) {
if (!this.hasRenderedOnce)
return;
Expand All @@ -73,17 +89,43 @@ export class PdfPage {
ctx.drawImage(this.canvas, 0, 0, this.canvas.width, this.canvas.height);
}

/**
* Get the base dimensions of the PDF page.
*/
getBaseDims() {
return {
width: this.baseWidth,
height: this.baseHeight
};
}

/**
* Get the dimensions of the PDF page corresponding to the current scale.
*/
getDims() {
// If the page hasn't rendered, the canvas still has its default size
if (!this.hasRenderedOnce)
throw 'Attempt to getDims() on a PdfPage that hasn\'t already rendered';

return {
width: this.canvas.width,
height: this.canvas.height
};
}

/**
* Call cb each time the PdfPage is ready to be drawn. The first and only
* parameter is true if there will be another invocation soon (the document
* will save again) and false otherwise.
*/
onRenderReady(cb) {
this.cbs.push(cb);
}

/**
* Call the callbacks supplied by onRenderReady(). This function should be
* considered private.
*/
notifyRenderReady(needsRerun) {
for (const cb of this.cbs) {
cb(needsRerun);
Expand Down

0 comments on commit 91da24c

Please sign in to comment.