|
57 | 57 | import { sessionTimeSpent } from 'kolibri.coreVue.vuex.getters'; |
58 | 58 | import { debounce } from 'lodash'; |
59 | 59 |
|
| 60 | + // Source from which PDFJS loads its service worker, this is based on the __publicPath |
| 61 | + // global that is defined in the Kolibri webpack pipeline, and the additional entry in the PDF renderer's |
| 62 | + // own webpack config |
60 | 63 | PDFJSLib.PDFJS.workerSrc = `${__publicPath}pdfJSWorker-${__version}.js`; |
61 | 64 |
|
62 | 65 | // Number of pages before and after current visible to keep rendered |
63 | 66 | const pageDisplayWindow = 1; |
| 67 | + // How often should we respond to changes in scrolling to render new pages? |
64 | 68 | const renderDebounceTime = 300; |
| 69 | + // Minimum height of the PDF viewer in pixels |
65 | 70 | const minViewerHeight = 400; |
66 | 71 |
|
67 | 72 | export default { |
|
136 | 141 | // Get viewport, which contains directions to be passed into render function |
137 | 142 | const viewport = pdfPage.getViewport(this.scale); |
138 | 143 |
|
139 | | - // put together the canvas element where page will be rendered |
140 | | - const canvas = document.createElement('canvas'); |
| 144 | + // create the canvas element where page will be rendered |
| 145 | + // we do this dynamically to avoid having many canvas elements simultaneously in the page |
| 146 | + const canvas = this.pdfPages[pageNum].canvas || document.createElement('canvas'); |
141 | 147 |
|
142 | 148 | // define canvas and dummy blank page dimensions |
143 | 149 | canvas.width = this.pageWidth = viewport.width; |
|
151 | 157 | viewport, |
152 | 158 | }); |
153 | 159 |
|
| 160 | + // Keep track of the canvas in case we need to manipulate it later |
154 | 161 | this.pdfPages[pageNum].canvas = canvas; |
155 | 162 | this.pdfPages[pageNum].renderTask = renderTask; |
156 | 163 | this.pdfPages[pageNum].rendering = true; |
|
162 | 169 |
|
163 | 170 | renderTask.then( |
164 | 171 | () => { |
| 172 | + // If this has been removed since the rendering started, then we should not proceed |
165 | 173 | if (this.pdfPages[pageNum]) { |
166 | | - this.pdfPages[pageNum].rendering = false; |
167 | 174 | if (this.pdfPages[pageNum].canvas) { |
168 | 175 | // Canvas has not been deleted in the interim |
169 | 176 | this.pdfPages[pageNum].rendered = true; |
|
174 | 181 | this.currentPageRendering = false; |
175 | 182 | } |
176 | 183 | } |
| 184 | + // Rendering has completed |
| 185 | + this.pdfPages[pageNum].rendering = false; |
177 | 186 | } |
178 | 187 | }, |
| 188 | + // If the render task is cancelled, then it will reject the promise and end up here. |
179 | 189 | () => { |
180 | 190 | if (this.pdfPages[pageNum]) { |
181 | 191 | this.pdfPages[pageNum].rendering = false; |
|
187 | 197 | }, |
188 | 198 | showPage(pageNum) { |
189 | 199 | if (pageNum <= this.totalPages && pageNum > 0) { |
| 200 | + // Only try to show pages that exist |
190 | 201 | if (!this.pdfPages[pageNum]) { |
191 | 202 | this.pdfPages[pageNum] = {}; |
192 | 203 | } |
193 | 204 | if ( |
| 205 | + // Do not try to show the page if it is already renderered, |
| 206 | + // already loading, or already rendering. |
194 | 207 | !this.pdfPages[pageNum].rendered && |
195 | 208 | !this.pdfPages[pageNum].loading && |
196 | 209 | !this.pdfPages[pageNum].rendering |
197 | 210 | ) { |
198 | 211 | this.pdfPages[pageNum].loading = true; |
| 212 | + // If we already have a reference to the PDFJS page object, then use it, |
| 213 | + // rather than refetching it. |
199 | 214 | if (!this.pdfPages[pageNum].pdfPage) { |
200 | 215 | this.pdfPages[pageNum].renderPromise = this.getPage(pageNum).then(this.startRender); |
201 | 216 | } else { |
|
206 | 221 | }, |
207 | 222 | hidePage(pageNum) { |
208 | 223 | if (pageNum <= this.totalPages && pageNum > 0) { |
| 224 | + // Only try to hide possibly existing pages. |
209 | 225 | if (!this.pdfPages[pageNum]) { |
210 | | - // No page rendered, so do nothing |
| 226 | + // No page to render, so do nothing |
211 | 227 | return; |
212 | 228 | } |
213 | 229 | const pdfPage = this.pdfPages[pageNum]; |
214 | 230 | if (pdfPage.rendered) { |
215 | 231 | pdfPage.rendered = false; |
| 232 | + // Already rendered, just remove canvas from DOM. |
216 | 233 | if (pdfPage.canvas) { |
217 | 234 | pdfPage.canvas.remove(); |
218 | 235 | } |
219 | 236 | } else if (pdfPage.loading) { |
220 | | - // Currently loading, cancel the task |
| 237 | + // Otherwise, currently loading - let it finish the render promise, |
| 238 | + // where the page is still being fetched |
| 239 | + // then cancel the resulting renderTask. |
221 | 240 | const renderTask = pdfPage.renderTask; |
222 | 241 | pdfPage.renderPromise.then(() => { |
223 | 242 | renderTask && renderTask.cancel(); |
224 | 243 | }); |
225 | 244 | } else if (pdfPage.rendering) { |
226 | | - // Currently rendering, cancel the task |
| 245 | + // Currently rendering, cancel the task directly |
227 | 246 | pdfPage.renderTask && pdfPage.renderTask.cancel(); |
228 | 247 | } |
| 248 | + // Clean everything up (destroys the pdf page object). |
229 | 249 | pdfPage.pdfPage && pdfPage.pdfPage.cleanup(); |
| 250 | + // Delete the reference so that this page is now a blank slate. |
230 | 251 | delete this.pdfPages[pageNum]; |
231 | 252 | } |
232 | 253 | }, |
|
235 | 256 | }, |
236 | 257 | // debouncing so we're not de/re-render many pages unnecessarily |
237 | 258 | checkPages: debounce(function() { |
| 259 | + // Calculate the position of the visible top and the bottom of the pdfContainer |
238 | 260 | const top = this.$refs.pdfContainer.scrollTop; |
239 | 261 | const bottom = top + this.$refs.pdfContainer.clientHeight; |
| 262 | + // Then work out which pages are visible to the user as a consequence |
240 | 263 | const topPageNum = Math.ceil(top / this.pageHeight); |
241 | 264 | const bottomPageNum = Math.ceil(bottom / this.pageHeight); |
242 | 265 |
|
243 | 266 | // Loop through all pages, show ones that are in the display window, hide ones that aren't |
244 | 267 | for (let i = 1; i <= this.totalPages; i++) { |
| 268 | + // Hide pages that are less than 'pageDisplayWindow' lower than the top page number |
| 269 | + // or the same amount higher than the bottom page number |
245 | 270 | if (i < topPageNum - pageDisplayWindow || i > bottomPageNum + pageDisplayWindow) { |
246 | 271 | this.hidePage(i); |
247 | 272 | } else { |
|
253 | 278 | watch: { |
254 | 279 | scrollPos: 'checkPages', |
255 | 280 | scale(newScale, oldScale) { |
| 281 | + // Listen to changes in scale, as we have to rerender every visible page if it changes. |
256 | 282 | const noChange = newScale === oldScale; |
257 | 283 | const firstChange = oldScale === null; |
258 | 284 |
|
|
281 | 307 | }; |
282 | 308 |
|
283 | 309 | this.prepComponentData = loadPdfPromise.then(pdfDocument => { |
| 310 | + // Get initial info from the loaded pdf document |
284 | 311 | this.pdfDocument = pdfDocument; |
285 | 312 | this.totalPages = pdfDocument.numPages; |
286 | 313 | this.pdfPages = {}; |
|
302 | 329 | const initialViewport = firstPage.getViewport(this.scale); |
303 | 330 | this.pageHeight = initialViewport.height; |
304 | 331 | this.pageWidth = initialViewport.width; |
| 332 | + // Set the firstPage into the pdfPages object so that we do not refetch the page |
| 333 | + // from PDFJS when we do our initial render |
| 334 | + this.pdfPages[1] = firstPage; |
305 | 335 | }); |
306 | 336 | }); |
307 | 337 | }, |
|
0 commit comments