1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
|
/*
Code for page layout. The idea is a bit like Java Swing (or a zillion other
similar setups), but stripped down to only what I need. In particular, the
Panels are assumed to be stacked vertically.
Everything is accessed through FullPanel, which consists of a list of
PagePanel objects. These are stacked vertically, and each one contains at
least one PDFPanel, and may contain FigurePanels. In the lingo of something
like Java, it would be better to think of PagePanel as a Container or a Box
since it's used for layout and doesn't directly do anything other than pass
things to its sub-parts.
*/
// This class is the only thing that should be touched by code outside
// this file. FullPanel.init() is called to set up page layout. Everything
// is static since there is only one window/canvas.
class FullPanel {
// The full document consists of a list of PagePanel objects.
// thePages[i] is the i-th page, counting from zero.
public static thePages : PagePanel[] = [];
// The width of the widest page of the entire document. Don't access
// this directly; use getFullWidth().
private static totalWidth : number = -1;
static init(specList : PageData[]) {
// Add all the pages to this document. specList is PDFDocument.pageSpecs.
// Each element of specList corresponds to page.
// Call this once, when the program starts, to set up the page layout.
let cumV = 0;
for (let i = 0; i < specList.length; i++)
{
let pp = new PagePanel(i,specList[i],cumV);
this.thePages[i] = pp;
cumV += pp.h;
}
}
static async renderAll( height : number ) {
// Render every PagePanel in FullPanel.thePages.
// Internal to this function, the height should be in "page pixels,"
// meaning the number of pixels tall the destination canvas is at the
// current zoom ratio of the offsreen pages.
height = height / PDFDocument.getZoom();
// Because of the use of promises by pdf.js, rendering is broken into
// two steps: pre-render and the actual rendering. Pre-rendering
// generates a bunch of promises (the pages rendered offscreen) and
// rendering can't be done until those promises resolve.
let ps = [];
for (let i = 0; i < this.thePages.length; i++)
{
let p = this.thePages[i].preRender(height);
ps.push(p);
}
// Don't return until this is done!
await Promise.all(ps);
for (let i = 0; i < this.thePages.length; i++)
this.thePages[i].render(height);
// Eliminate any excess pages from the buffer of offscreen pages.
// Do this after copying out, just in case there *is* some weird
// problem with race conditions.
PDFDocument.trimBuffer();
}
static totalHeight() : number {
// Return the total height of all pages. This is used to set up
// the scroll bars.
let answer = 0;
for (let i = 0; i < this.thePages.length; i++)
answer += this.thePages[i].h;
return answer;
}
static getFullWidth() : number {
// Return the width of the widest page. Most of the time, the pages of
// a document all have the same width, but they might not in some
// rare case. This is needed to center things left/right in the window.
if (this.totalWidth > 0)
return this.totalWidth;
// Need to calculate it for the first time.
for (let i = 0; i < this.thePages.length; i++)
{
let pp = this.thePages[i];
if (pp.w > this.totalWidth)
this.totalWidth = pp.w;
}
return this.totalWidth;
}
static mouseDown(x : number , y : number ) : void {
// (x,y) is in pdf points, relative to the entire document.
// So y will be a huge number if it's on the umpteeth page.
// To save CPU, I could start off by checking whether x is in
// [0,pageWidth] and return if it is not, but the user might want to
// place controls outside the page. This is unlikely, but possible.
// Whoever had focus loses it. It may be taken up by some other widget
// (or the same widget again), but nobody has focus by default.
WidgetManager.focusOwner = null;
// Figure out which page this is and hand it off.
let i = 0;
for ( ; i < this.thePages.length; i++)
{
if (this.thePages[i].v > y)
{
// Found the first and only page this could be. It was the
// page previous to this one.
i -= 1;
break;
}
}
// If we got here then it had to be the very last page.
if (i === this.thePages.length)
i = this.thePages.length - 1;
// Safety check:
if ((i == this.thePages.length) || (i < 0))
return;
this.thePages[i].mouseDown(x,y);
}
static mouseMove(x : number , y : number ) : void {
// As above. However, these are only of interest if some widget
// "owns" the mouse -- something was clicked on so that motion
// could mean something.
if (WidgetManager.mouseOwner === null)
return;
// Although we know exactly which widget will ultimately get this
// event, it's easier to let this pass through the layout hierarchy,
// just as for mouseDown(), so that the coordinates are properly adjusted.
// BUG: Not DRY.
let i = 0;
for ( ; i < this.thePages.length; i++)
{
if (this.thePages[i].v > y)
{
i -= 1;
break;
}
}
if (i === this.thePages.length)
i = this.thePages.length - 1;
if ((i == this.thePages.length) || (i < 0))
return;
this.thePages[i].mouseMove(x,y);
}
static mouseUp(x : number , y : number ) : void {
// The big issue here is that the mouse was released so that ownership
// is once again up for grabs. In addition, certain widgets will want
// to know where the mouse was released. Buttons are a good example.
// You could have mouse-down on the button, then the user moves the
// mouse out of the button and releases it; the button should only be
// "clicked" if the mouse was released over the button.
//
// An annoying thing here is that the *only* widget that could care about
// this (at least, as designed) is the one that owns the mouse-down.
// As above, we want the event to pass through the layout hierarchy
// so that (x,y) is adjusted for the right frame, but the ultimate
// *consumer* of the event may not even be on the page where the
// mouse-up occured.
// BUG: Not DRY.
if (WidgetManager.mouseOwner === null)
return;
let i = 0;
for ( ; i < this.thePages.length; i++)
{
if (this.thePages[i].v > y)
{
i -= 1;
break;
}
}
if (i === this.thePages.length)
i = this.thePages.length - 1;
if ((i == this.thePages.length) || (i < 0))
return;
this.thePages[i].mouseUp(x,y);
// Whatever happened above, the mouse is now up for grabs.
WidgetManager.mouseOwner = null;
}
}
// A PagePanel is the top-level thing, just under the canvas. Each
// PagePanel makes up a single page of the printed document. There's
// a list of them in FullPanel. It includes references to the PDFPanel
// and FigurePanel objects that it contains.
class PagePanel {
// Every panel has a vertical position within the entire document and height,
// in pdf pts. The vertical postion, v, is the top of the page, so the page
// extends from v to v+h. The caller must ensure that the Panels stack up
// correctly since there is no real page layout.
//
// In some ways the height, h, is redundant since it could be worked
// out from the heights of the individual sub-parts. In fact (see below)
// it *is* worked out by the constuctor, but it's easier to do it once
// and be done with it. This is the height of the page, as rendered,
// taking into account any mismatch due to extra "padding" in the figures
// (if there is any, and there often is not).
v = 0;
w = 0;
h = 0;
// page number, counting from zero.
pageNum = 0;
// The SubPanels that make up this PagePanel.
parts : SubPanel[] = [];
constructor(pageNum : number , pageSpec : PageData , v : number) {
// This implicitly applies to the global (because everything is static)
// PDFDocument. pageNum is which page this is, counting from zero.
// pageSpec has the info about how the page breaks into pieces and the
// relevant figures. v is where this page lies in the entire vertical
// list of pages.
this.w = pageSpec.pageWidth;
this.v = v;
this.pageNum = pageNum;
// Create the PDFPanels and FigurePanels in this PagePanel.
let s = pageSpec;
if (s.insertPoint.length == 0)
{
// There are no figures on this page.
let p = new PDFPanel(pageNum,this.v,0,0,this.v,this.w,s.pageHeight);
this.h = s.pageHeight;
this.parts = [p];
}
else
{
// There are figures.
let srcV = 0;
let destV = 0;
let totalV = v;
for (let j = 0; j < s.insertPoint.length; j++)
{
// Bit of pdf above figure.
let p = new PDFPanel(pageNum,this.v,srcV,destV,totalV,this.w,
s.insertPoint[j]-srcV);
destV += s.insertPoint[j]-srcV;
totalV += s.insertPoint[j]-srcV;
let f = new FigurePanel( this.v , destV , totalV , this.w ,
s.deleteHeight[j] + s.aboveHeight[j] + s.belowHeight[j] ,
s.aboveHeight[j] , s.belowHeight[j] , s.leftMargin ,
s.textWidth , s.drawFcn[j] );
srcV = s.insertPoint[j] + s.deleteHeight[j];
destV += s.deleteHeight[j] + s.aboveHeight[j] + s.belowHeight[j];
totalV += s.deleteHeight[j] + s.aboveHeight[j] + s.belowHeight[j];
this.parts.push(p);
this.parts.push(f);
}
// And the bit of pdf below the last figure on the page.
let p = new PDFPanel(pageNum,this.v,srcV,destV,totalV,
this.w,s.pageHeight - srcV);
this.parts.push(p);
this.h = destV + s.pageHeight - srcV
}
}
async preRender( height : number ) {
// This renders the underlying page of pdf. It returns a promise
// so that the caller can wait for the promise to resolve before
// attempting to copy from the rendered page.
//
// The vpos is where the top of the ctx should be relative to the entire
// document, in pdf pts, and height is how much is visible, in rendered
// page pixels.
// The first question is whether any of this page is visible.
let vpos = window.scrollY;
if (this.v + this.h < vpos)
// Entire page is above the visible area.
return;
if (this.v > vpos + height)
// Entire page is below the visible area.
return;
// Got here, so some portion of the page is visible.
await PDFDocument.render(this.pageNum);
}
render( height : number ) : void {
// Render every SubPanel of the current PagePanel.
// BUG: Before returning, turn off any of these animations that are
// definitely not visible.
let vpos = window.scrollY;
if (this.v + this.h < vpos)
return;
if (this.v > vpos + height)
return;
// Got here, so some portion of the page is visible. From here on,
// render everything, whether it's actually visible or not.
let ctx = ctxTopLevelAdjust();
// Call the parts of the page. These could be PDFPanel or FigurePanel
// objects.
for (let i = 0; i < this.parts.length; i++)
this.parts[i].render();
// BUG: I don't like this use of zoom here. Maybe no choice?
let z = PDFDocument.getZoom();
// Put a rectangle aroud the entire page. I'm not 100% convinced that
// I like this.
ctx.strokeStyle = "black";
ctx.strokeRect(0,0,this.w*z,this.h*z);
}
mouseDown(x : number , y : number ) : void {
// Mouse clicked on this page.
// y is given relative to the entire document; make it page-relative.
y -= this.v;
// Only clicks on a figure could be of interest.
for (let i = 0; i < this.parts.length; i++)
{
// Either a PDFPanel or a FigurePanel.
let p = this.parts[i];
if (p instanceof PDFPanel)
continue;
// p must be a FigurePanel.
if ((p.destV <= y) && (y <= p.destV + p.h))
return p.mouseDown(x,y);
}
}
mouseMove(x : number , y : number ) : void {
// As above. Note that this event could go to the "wrong" figure,
// but that's OK. Also, if the mouse is over a PDFPanel, and not
// a figure, then the event dies here, which is also OK.
y -= this.v;
for (let i = 0; i < this.parts.length; i++)
{
let p = this.parts[i];
if (p instanceof PDFPanel)
continue;
if ((p.destV <= y) && (y <= p.destV + p.h))
p.mouseMove(x,y);
}
}
mouseUp(x : number , y : number ) : void {
// As above, but the event can't be allowed to die. The owning widget
// must hear about the mouse up. At the same time, we can't just
// inform the widget directly of the mouse up since we also need to
// pass the correct coordinates.
y -= this.v;
for (let i = 0; i < this.parts.length; i++)
{
let p = this.parts[i];
// This is different than above since *somebody* must take the event,
// and the relevant widget must hear about it. If the event is over
// a PDFPanel, then tell the widget using crazy coordinates. It
// doesn't matter exactly where the mouse was released; it only
// matters that it wasn't released anywhere near the widget.
if ((p.destV <= y) && (y <= p.destV + p.h))
{
if (p instanceof PDFPanel)
WidgetManager.mouseOwner ! .mouseUp(10000000000000,10000000000000);
else
// Over a figure.
p.mouseUp(x,y);
}
}
}
}
// A PagePanel consists of one or more SubPanels.
abstract class SubPanel {
// The vertical position and height within a page, with zero being at the
// top, and measured in pdf points. This height, h, is the total height.
// There's no ambiguity to this height for PDFPanel subclasses, but for
// FigurePanel subclasses, it is the sum of the latex height
// (PageData.deleteHeight), plus any additional padding as given in
// PageData.aboveHeight/belowHeight.
destV = 0;
h = 0;
w = 0;
// The position of this panel within the entire document.
// The only reason for this is the possible use of HTML DOM elements
// as widgets within a figure. I would prefer not to use those at all,
// but sometimes it's easier. See the NumberInputWidget for one example.
totalV = 0;
constructor(v : number , totalV : number , w : number , h : number ) {
this.destV = v;
this.totalV = totalV;
this.w = w;
this.h = h;
}
// Will be filled in by sub-class.
public render( ) : void {
console.log("Error: called SubPanel.render()!");
}
public mouseDown(x : number , y : number ) : void {
console.log("Error: called SubPanel.mouseDown()!");
}
public mouseMove(x : number , y : number ) : void {
console.log("Error: called SubPanel.mouseMove()!");
}
public mouseUp(x : number , y : number ) : void {
console.log("Error: called SubPanel.mouseUp()!");
}
}
// Used for portions of a page consisting of rendered pdf.
class PDFPanel extends SubPanel {
// The page numbers start at zero and positions are given in pdf points.
pageNum = 0;
srcV = 0;
// BUG: I think I can fold this in elsewhere. It's the same as PagePanel.v.
offsetV = 0;
constructor(pageNum : number , offsetV : number , srcV : number , destV : number ,
totalV : number , w : number , h : number ) {
// The pageNum is given relative to the global PDFDocument. The srcV
// and destV are locations relative to the page (in pdf points, with the
// top of the page at v=0) and h is the height of this piece, which
// is the same for src and dest.
super(destV,totalV,w,h);
this.pageNum = pageNum;
this.srcV = srcV;
this.offsetV = offsetV;
}
render( ) : void {
// Render a portion of the current page.
let theCanvas = PDFDocument.getCanvas(this.pageNum) ;
if (theCanvas === null)
{
// I'm sure how this happens, but it does occasionally.
// It doesn't cause any noticable problems. It seems to happen
// if you move the scroll thumb too fast.
return;
}
// BUG: I don't like this use of zoom here. Maybe no choice?
let z = PDFDocument.getZoom();
let ctx = ctxTopLevelAdjust();
// Adjust for scroll bar.
ctx.translate(0,(this.offsetV - window.scrollY) * z);
// Arguments here:
// the source image (or canvas),
// the source (x,y),
// the source (width,height),
// the destination (x,y),
// the destination (width,height),
// It's confusing because optional things typically come after
// required things, but not here somehow.
ctx.drawImage(theCanvas,
0,this.srcV * z,theCanvas.width,this.h * z,
0,this.destV*z,theCanvas.width,this.h*z);
}
}
class FigurePanel extends SubPanel {
// Function name, as a string.
drawFcn : AugmentedDrawingFunction;
// Page's v position. This is the position of the page on which
// this figure appears relative to the entire document.
pageV : number = 0;
// This margin is the location of the left edge of the text, as
// reported by latex.
margin = 0;
// Also from Latex (via figures.aux). This I have more confidence in.
textWidth = 0;
// The height in SubPanel.h is the total height of the figure, including
// any padding above or below. The origin used for the figure occurs at
// a y-value that is PageData.belowHeight *above* the total height.
// lowerPadding is equal to PageData.belowHeight for this figure.
lowerPadding = 0;
upperPadding = 0;
constructor(pageV : number, destV : number , totalV : number , w : number , h : number ,
upperPadding : number , lowerPadding : number , margin : number ,
textWidth : number , drawFcn : AugmentedDrawingFunction) {
// As for PDFPanel, plus the margin is the amount by which to shift the
// drawing to the right so that the origin is in line with the text.The
// drawFcn is the function provided through Latex.
// This is just the name of the function, as a string.
super(destV,totalV,w,h);
this.pageV = pageV;
this.upperPadding = upperPadding;
this.lowerPadding = lowerPadding;
this.margin = margin;
this.textWidth = textWidth;
this.drawFcn = drawFcn;
}
render( ) : void {
// Save this for widgets and animations to use.
let ctx = ctxTopLevelAdjust();
let z = PDFDocument.getZoom();
ctx.translate(0,(this.pageV - window.scrollY) * z);
// Erase the full width of the page. this.w is the correct width,
// but the origin will be shifted for drawing. So, erase, then
// return the origin to where it was and start over.
// Shift to where the figure appears and erase.
ctx.translate(0,this.destV * z);
ctx.scale(z,z);
// The small adjustment here is to prevent erasing the rectangle
// that encloses the entire page.
ctx.clearRect(1,0,this.w-2,this.h);
// Return to the orginal t-matrix, then shift down and right before
// drawing the figure (and widgets).
// What we want is for the origin to be at the lower-right and
// right-handed, adjusted upwards by this.lowerPadding too.
ctx = ctxTopLevelAdjust();
ctx.translate(0,(this.pageV - window.scrollY) * z);
ctx.translate(this.margin * z,(this.destV + this.h - this.lowerPadding) * z);
ctx.scale(1,-1);
ctx.scale(z,z);
// Tack this FigurePanel onto the underlying figure-drawing code.
// The first time the figure is rendered, this is set, and it's re-set
// to the same value with every subsequent call. That seems like
// pointless extra work, but it gets the job done.
this.drawFcn.figurePanelClass = this;
this.drawFcn(ctx);
}
mouseDown(x : number , y : number ) : void {
// (x,y) is in pdf points, relative to the top-left of the page.
// Adjust to be relative to the figure, but still LH, relative
// to the top of the figure.
y -= this.destV;
x -= this.margin;
// y is now given relative to the top edge of the figure, getting
// larger as you go *down* the page.
// Convert y to be RH relative to the correct origin, taking
// any padding into account. This is confusing. At this stage,
// y is the distance below the figure's top edge. Call that y0.
// We want the distance above the lower padding (if any); call
// that y1. The distance above the lower *edge* of the figure
// is this.h - y0, and from this we subtract the padding.
y = (this.h - y) - this.lowerPadding;
// Pass to the relevant widget.
WidgetManager.mouseDown(this.drawFcn,x,y);
}
mouseMove(x : number , y : number ) : void {
// As above.
y -= this.destV;
x -= this.margin;
y = (this.h - this.lowerPadding) - y;
WidgetManager.mouseMove(this.drawFcn,x,y);
}
mouseUp(x : number , y : number ) : void {
// As above.
y -= this.destV;
x -= this.margin;
y = (this.h - this.lowerPadding) - y;
WidgetManager.mouseUp(this.drawFcn,x,y);
}
}
|