Skip to content

Commit 0a1fec7

Browse files
committed
Refactor to handle pages of different sizes.
1 parent 56c06a5 commit 0a1fec7

File tree

2 files changed

+210
-148
lines changed

2 files changed

+210
-148
lines changed

kolibri/plugins/document_pdf_render/assets/src/views/index.vue

Lines changed: 68 additions & 148 deletions
Original file line numberDiff line numberDiff line change
@@ -33,14 +33,17 @@
3333

3434
<div ref="pdfContainer" id="pdf-container" @scroll="checkPages">
3535
<progress-bar v-if="documentLoading" class="progress-bar" :show-percentage="true" :progress="progress"/>
36-
<section
36+
<page-component
3737
class="pdf-page-container"
38-
v-for="index in totalPages"
38+
v-for="(page, index) in pdfPages"
3939
:key="index"
40-
:ref="pageRef(index)"
41-
:style="{ height: pageHeight + 'px', width: pageWidth + 'px' }">
42-
<span class="pdf-page-loading"> {{ $tr('pageNumber', {pageNumber: index}) }} </span>
43-
</section>
40+
:ref="pageRef(index + 1)"
41+
:pdfPage="page"
42+
:defaultHeight="pageHeight"
43+
:defaultWidth="pageWidth"
44+
:scale="scale"
45+
:pageNum="index + 1">
46+
</page-component>
4447
</div>
4548
</div>
4649

@@ -58,6 +61,7 @@
5861
import responsiveWindow from 'kolibri.coreVue.mixins.responsiveWindow';
5962
import { sessionTimeSpent } from 'kolibri.coreVue.vuex.getters';
6063
import { debounce } from 'lodash';
64+
import pageComponent from './pdfPage';
6165
6266
// Source from which PDFJS loads its service worker, this is based on the __publicPath
6367
// global that is defined in the Kolibri webpack pipeline, and the additional entry in the PDF renderer's
@@ -78,6 +82,7 @@
7882
iconButton,
7983
progressBar,
8084
uiIconButton,
85+
pageComponent,
8186
},
8287
props: ['defaultFile'],
8388
data: () => ({
@@ -88,6 +93,7 @@
8893
totalPages: null,
8994
pageHeight: null,
9095
pageWidth: null,
96+
pdfPages: [],
9197
}),
9298
computed: {
9399
fullscreenAllowed() {
@@ -126,130 +132,30 @@
126132
getPage(pageNum) {
127133
return this.pdfDocument.getPage(pageNum);
128134
},
129-
startRender(pdfPage) {
130-
// use a promise because this also calls render, allowing us to cancel
131-
return new Promise((resolve, reject) => {
132-
const pageNum = pdfPage.pageNumber;
133-
134-
// start the loading message
135-
if (this.currentPageNum === pageNum) {
136-
this.currentPageRendering = true;
137-
}
138-
139-
if (this.pdfPages[pageNum]) {
140-
this.pdfPages[pageNum].pdfPage = pdfPage;
141-
142-
// Get viewport, which contains directions to be passed into render function
143-
const viewport = pdfPage.getViewport(this.scale);
144-
145-
// create the canvas element where page will be rendered
146-
// we do this dynamically to avoid having many canvas elements simultaneously in the page
147-
const canvas = this.pdfPages[pageNum].canvas || document.createElement('canvas');
148-
149-
// define canvas and dummy blank page dimensions
150-
canvas.width = this.pageWidth = viewport.width;
151-
canvas.height = this.pageHeight = viewport.height;
152-
canvas.style.position = 'absolute';
153-
canvas.style.top = 0;
154-
canvas.style.left = 0;
155-
156-
const renderTask = pdfPage.render({
157-
canvasContext: canvas.getContext('2d'),
158-
viewport,
159-
});
160-
161-
// Keep track of the canvas in case we need to manipulate it later
162-
this.pdfPages[pageNum].canvas = canvas;
163-
this.pdfPages[pageNum].renderTask = renderTask;
164-
this.pdfPages[pageNum].rendering = true;
165-
this.pdfPages[pageNum].loading = false;
166-
167-
// resolves here to indicate that the page has been set up for render.
168-
// check flags for the stages of the render
169-
resolve();
170-
171-
renderTask.then(
172-
() => {
173-
// If this has been removed since the rendering started, then we should not proceed
174-
if (this.pdfPages[pageNum]) {
175-
if (this.pdfPages[pageNum].canvas) {
176-
// Canvas has not been deleted in the interim
177-
this.pdfPages[pageNum].rendered = true;
178-
this.$refs[this.pageRef(pageNum)][0].appendChild(this.pdfPages[pageNum].canvas);
179-
180-
// end the loading message
181-
if (this.currentPageNum === pageNum) {
182-
this.currentPageRendering = false;
183-
}
184-
}
185-
// Rendering has completed
186-
this.pdfPages[pageNum].rendering = false;
187-
}
188-
},
189-
// If the render task is cancelled, then it will reject the promise and end up here.
190-
() => {
191-
if (this.pdfPages[pageNum]) {
192-
this.pdfPages[pageNum].rendering = false;
193-
}
194-
}
195-
);
196-
}
197-
});
198-
},
199135
showPage(pageNum) {
200136
if (pageNum <= this.totalPages && pageNum > 0) {
201-
// Only try to show pages that exist
202-
if (!this.pdfPages[pageNum]) {
203-
this.pdfPages[pageNum] = {};
204-
}
205-
if (
206-
// Do not try to show the page if it is already rendered,
207-
// already loading, or already rendering.
208-
!this.pdfPages[pageNum].rendered &&
209-
!this.pdfPages[pageNum].loading &&
210-
!this.pdfPages[pageNum].rendering
211-
) {
212-
this.pdfPages[pageNum].loading = true;
213-
// If we already have a reference to the PDFJS page object, then use it,
214-
// rather than refetching it.
215-
if (!this.pdfPages[pageNum].pdfPage) {
216-
this.pdfPages[pageNum].renderPromise = this.getPage(pageNum).then(this.startRender);
217-
} else {
218-
this.pdfPages[pageNum].renderPromise = this.startRender(this.pdfPages[pageNum].pdfPage);
219-
}
137+
const pageIndex = pageNum - 1;
138+
if (!this.pdfPages[pageIndex]) {
139+
// Only bother getting it if the pdfPage object is not already cached in the array
140+
// Cache the getPage promise in the array to prevent multiple gets, then replace it with
141+
// the page once it has been fetched
142+
this.pdfPages.splice(
143+
pageIndex,
144+
1,
145+
this.getPage(pageNum).then(pdfPage => {
146+
this.pdfPages.splice(pageIndex, 1, pdfPage);
147+
this.$refs[this.pageRef(pageNum)][0].active = true;
148+
})
149+
);
150+
} else {
151+
this.$refs[this.pageRef(pageNum)][0].active = true;
220152
}
221153
}
222154
},
223155
hidePage(pageNum) {
224156
if (pageNum <= this.totalPages && pageNum > 0) {
225157
// Only try to hide possibly existing pages.
226-
if (!this.pdfPages[pageNum]) {
227-
// No page to render, so do nothing
228-
return;
229-
}
230-
const pdfPage = this.pdfPages[pageNum];
231-
if (pdfPage.rendered) {
232-
pdfPage.rendered = false;
233-
// Already rendered, just remove canvas from DOM.
234-
if (pdfPage.canvas) {
235-
pdfPage.canvas.remove();
236-
}
237-
} else if (pdfPage.loading) {
238-
// Otherwise, currently loading - let it finish the render promise,
239-
// where the page is still being fetched
240-
// then cancel the resulting renderTask.
241-
const renderTask = pdfPage.renderTask;
242-
pdfPage.renderPromise.then(() => {
243-
renderTask && renderTask.cancel();
244-
});
245-
} else if (pdfPage.rendering) {
246-
// Currently rendering, cancel the task directly
247-
pdfPage.renderTask && pdfPage.renderTask.cancel();
248-
}
249-
// Clean everything up (destroys the pdf page object).
250-
pdfPage.pdfPage && pdfPage.pdfPage.cleanup();
251-
// Delete the reference so that this page is now a blank slate.
252-
delete this.pdfPages[pageNum];
158+
this.$refs[this.pageRef(pageNum)][0].active = false;
253159
}
254160
},
255161
pageRef(index) {
@@ -261,17 +167,44 @@
261167
const top = this.$refs.pdfContainer.scrollTop;
262168
const bottom = top + this.$refs.pdfContainer.clientHeight;
263169
// Then work out which pages are visible to the user as a consequence
264-
const topPageNum = Math.ceil(top / this.pageHeight);
265-
const bottomPageNum = Math.ceil(bottom / this.pageHeight);
266-
267170
// Loop through all pages, show ones that are in the display window, hide ones that aren't
268-
for (let i = 1; i <= this.totalPages; i++) {
269-
// Hide pages that are less than 'pageDisplayWindow' lower than the top page number
270-
// or the same amount higher than the bottom page number
271-
if (i < topPageNum - pageDisplayWindow || i > bottomPageNum + pageDisplayWindow) {
272-
this.hidePage(i);
273-
} else {
171+
let cumulativeHeight = 0;
172+
const pagesToDisplay = [];
173+
let i, display;
174+
for (i = 1; i <= this.totalPages; i++) {
175+
// If the current cumulativeHeight (which marks the beginning of this page)
176+
// is higher than top and less than bottom, then this page
177+
// should be displayed
178+
display = false;
179+
const pageHeight = this.$refs[this.pageRef(i)][0].pageHeight;
180+
// Top of page is in the middle of the viewport
181+
if (cumulativeHeight >= top && cumulativeHeight <= bottom) {
182+
display = true;
183+
}
184+
// Page top and bottom wrap the viewport
185+
if (cumulativeHeight <= top && cumulativeHeight + pageHeight >= bottom) {
186+
display = true;
187+
}
188+
cumulativeHeight += pageHeight;
189+
// Bottom of page is in the middle of the viewport
190+
if (cumulativeHeight >= top && cumulativeHeight <= bottom) {
191+
display = true;
192+
}
193+
pagesToDisplay.push(display);
194+
}
195+
for (i = 1; i <= this.totalPages; i++) {
196+
// Render pages conditionally on pagesToDisplay, taking into account the display window
197+
if (
198+
pagesToDisplay
199+
.slice(
200+
Math.max(0, i - 1 - pageDisplayWindow),
201+
Math.min(pagesToDisplay.length, i - 1 + pageDisplayWindow)
202+
)
203+
.some(trueOrFalse => trueOrFalse)
204+
) {
274205
this.showPage(i);
206+
} else {
207+
this.hidePage(i);
275208
}
276209
}
277210
}, renderDebounceTime),
@@ -311,7 +244,8 @@
311244
// Get initial info from the loaded pdf document
312245
this.pdfDocument = pdfDocument;
313246
this.totalPages = pdfDocument.numPages;
314-
this.pdfPages = {};
247+
// Set pdfPages to an array of length total pages
248+
this.pdfPages = Array(this.totalPages);
315249
316250
return this.getPage(1).then(firstPage => {
317251
const pageMargin = 5;
@@ -332,7 +266,7 @@
332266
this.pageWidth = initialViewport.width;
333267
// Set the firstPage into the pdfPages object so that we do not refetch the page
334268
// from PDFJS when we do our initial render
335-
this.pdfPages[1] = firstPage;
269+
this.pdfPages.splice(0, 1, firstPage);
336270
});
337271
});
338272
},
@@ -361,7 +295,6 @@
361295
$trs: {
362296
exitFullscreen: 'Exit fullscreen',
363297
enterFullscreen: 'Enter fullscreen',
364-
pageNumber: '{pageNumber, number}',
365298
},
366299
vuex: {
367300
getters: {
@@ -379,6 +312,7 @@
379312
380313
$keen-button-height = 48px
381314
$fullscreen-button-height = 36px
315+
// Defined here and in pdfPage.vue
382316
$page-padding = 5px
383317
384318
.doc-viewer
@@ -419,20 +353,6 @@
419353
// prevents a never-visible spot underneath the fullscreen button
420354
padding-top: $fullscreen-button-height + $page-padding
421355
422-
.pdf-page
423-
&-container
424-
background: #FFFFFF
425-
margin: $page-padding auto
426-
position: relative
427-
z-index: 2 // material spec - card (resting)
428-
&-loading
429-
position: absolute
430-
top: 50%
431-
left: 50%
432-
transform: translate(-50%, -50%)
433-
font-size: 2em
434-
line-height: 100%
435-
436356
.doc-viewer-controls
437357
z-index: 6 // material spec - snackbar and FAB
438358

0 commit comments

Comments
 (0)