pdf2htmlEX.min.js 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983
  1. /* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab filetype=javascript : */
  2. /**
  3. * @license pdf2htmlEX.js: Core UI functions for pdf2htmlEX
  4. * Copyright 2012,2013 Lu Wang <coolwanglu@gmail.com> and other contributors
  5. * https://github.com/coolwanglu/pdf2htmlEX/blob/master/share/LICENSE
  6. */
  7. /*
  8. * Attention:
  9. * This files is to be optimized by closure-compiler,
  10. * so pay attention to the forms of property names:
  11. *
  12. * string/bracket form is safe, won't be optimized:
  13. * var obj={ 'a':'b' }; obj['a'] = 'b';
  14. * name/dot form will be optimized, the name is likely to be modified:
  15. * var obj={ a:'b' }; obj.a = 'b';
  16. *
  17. * Either form can be used for internal objects,
  18. * but must be consistent for each one respectively.
  19. *
  20. * string/bracket form must be used for external objects
  21. * e.g. DEFAULT_CONFIG, object stored in page-data
  22. * property names are part of the `protocol` in these cases.
  23. *
  24. */
  25. 'use strict';
  26. var pdf2htmlEX = window['pdf2htmlEX'] = window['pdf2htmlEX'] || {};
  27. /**
  28. * @const
  29. * @struct
  30. */
  31. var CSS_CLASS_NAMES = {
  32. page_frame : 'pf',
  33. page_content_box : 'pc',
  34. page_data : 'pi',
  35. background_image : 'bi',
  36. link : 'l',
  37. input_radio : 'ir',
  38. __dummy__ : 'no comma'
  39. };
  40. /**
  41. * configurations of Viewer
  42. * @const
  43. * @dict
  44. */
  45. var DEFAULT_CONFIG = {
  46. // id of the element to put the pages in
  47. 'container_id' : 'page-container',
  48. // id of the element for sidebar (to open and close)
  49. 'sidebar_id' : 'sidebar',
  50. // id of the element for outline
  51. 'outline_id' : 'outline',
  52. // class for the loading indicator
  53. 'loading_indicator_cls' : 'loading-indicator',
  54. // How many page shall we preload that are below the last visible page
  55. 'preload_pages' : 3,
  56. // how many ms should we wait before actually rendering the pages and after a scroll event
  57. 'render_timeout' : 100,
  58. // zoom ratio step for each zoom in/out event
  59. 'scale_step' : 0.9,
  60. // register global key handler, allowing navigation by keyboard
  61. 'key_handler' : true,
  62. // register hashchange handler, navigate to the location specified by the hash
  63. 'hashchange_handler' : true,
  64. // register view history handler, allowing going back to the previous location
  65. 'view_history_handler' : true,
  66. '__dummy__' : 'no comma'
  67. };
  68. /** @const */
  69. var EPS = 1e-6;
  70. /************************************/
  71. /* utility function */
  72. /**
  73. * @param{Array.<number>} ctm
  74. */
  75. function invert(ctm) {
  76. var det = ctm[0] * ctm[3] - ctm[1] * ctm[2];
  77. return [ ctm[3] / det
  78. ,-ctm[1] / det
  79. ,-ctm[2] / det
  80. ,ctm[0] / det
  81. ,(ctm[2] * ctm[5] - ctm[3] * ctm[4]) / det
  82. ,(ctm[1] * ctm[4] - ctm[0] * ctm[5]) / det
  83. ];
  84. };
  85. /**
  86. * @param{Array.<number>} ctm
  87. * @param{Array.<number>} pos
  88. */
  89. function transform(ctm, pos) {
  90. return [ctm[0] * pos[0] + ctm[2] * pos[1] + ctm[4]
  91. ,ctm[1] * pos[0] + ctm[3] * pos[1] + ctm[5]];
  92. };
  93. /**
  94. * @param{Element} ele
  95. */
  96. function get_page_number(ele) {
  97. return parseInt(ele.getAttribute('data-page-no'), 16);
  98. };
  99. /**
  100. * @param{NodeList} eles
  101. */
  102. function disable_dragstart(eles) {
  103. for (var i = 0, l = eles.length; i < l; ++i) {
  104. eles[i].addEventListener('dragstart', function() {
  105. return false;
  106. }, false);
  107. }
  108. };
  109. /**
  110. * @param{...Object} var_args
  111. */
  112. function clone_and_extend_objs(var_args) {
  113. var result_obj = {};
  114. for (var i = 0, l = arguments.length; i < l; ++i) {
  115. var cur_obj = arguments[i];
  116. for (var k in cur_obj) {
  117. if (cur_obj.hasOwnProperty(k)) {
  118. result_obj[k] = cur_obj[k];
  119. }
  120. }
  121. }
  122. return result_obj;
  123. };
  124. /**
  125. * @constructor
  126. * @param{Element} page The element for the page
  127. */
  128. function Page(page) {
  129. if (!page) return;
  130. this.loaded = false;
  131. this.shown = false;
  132. this.page = page; // page frame element
  133. this.num = get_page_number(page);
  134. // page size
  135. // Need to make rescale work when page_content_box is not loaded, yet
  136. this.original_height = page.clientHeight;
  137. this.original_width = page.clientWidth;
  138. // content box
  139. var content_box = page.getElementsByClassName(CSS_CLASS_NAMES.page_content_box)[0];
  140. // if page is loaded
  141. if (content_box) {
  142. this.content_box = content_box;
  143. /*
  144. * scale ratios
  145. *
  146. * original_scale : the first one
  147. * cur_scale : currently using
  148. */
  149. this.original_scale = this.cur_scale = this.original_height / content_box.clientHeight;
  150. this.page_data = JSON.parse(page.getElementsByClassName(CSS_CLASS_NAMES.page_data)[0].getAttribute('data-data'));
  151. this.ctm = this.page_data['ctm'];
  152. this.ictm = invert(this.ctm);
  153. this.loaded = true;
  154. }
  155. };
  156. Page.prototype = {
  157. /* hide & show are for contents, the page frame is still there */
  158. hide : function(){
  159. if (this.loaded && this.shown) {
  160. this.content_box.classList.remove('opened');
  161. this.shown = false;
  162. }
  163. },
  164. show : function(){
  165. if (this.loaded && !this.shown) {
  166. this.content_box.classList.add('opened');
  167. this.shown = true;
  168. }
  169. },
  170. /**
  171. * @param{number} ratio
  172. */
  173. rescale : function(ratio) {
  174. if (ratio === 0) {
  175. // reset scale
  176. this.cur_scale = this.original_scale;
  177. } else {
  178. this.cur_scale = ratio;
  179. }
  180. // scale the content box
  181. if (this.loaded) {
  182. var cbs = this.content_box.style;
  183. cbs.msTransform = cbs.webkitTransform = cbs.transform = 'scale('+this.cur_scale.toFixed(3)+')';
  184. }
  185. // stretch the page frame to hold the place
  186. {
  187. var ps = this.page.style;
  188. ps.height = (this.original_height * this.cur_scale) + 'px';
  189. ps.width = (this.original_width * this.cur_scale) + 'px';
  190. }
  191. },
  192. /*
  193. * return the coordinate of the top-left corner of container
  194. * in our coordinate system
  195. * assuming that p.parentNode === p.offsetParent
  196. */
  197. view_position : function () {
  198. var p = this.page;
  199. var c = p.parentNode;
  200. return [c.scrollLeft - p.offsetLeft - p.clientLeft
  201. ,c.scrollTop - p.offsetTop - p.clientTop];
  202. },
  203. height : function () {
  204. return this.page.clientHeight;
  205. },
  206. width : function () {
  207. return this.page.clientWidth;
  208. }
  209. };
  210. /**
  211. * @constructor
  212. * @param{Object=} config
  213. */
  214. function Viewer(config) {
  215. this.config = clone_and_extend_objs(DEFAULT_CONFIG, (arguments.length > 0 ? config : {}));
  216. this.pages_loading = [];
  217. this.init_before_loading_content();
  218. var self = this;
  219. document.addEventListener('DOMContentLoaded', function(){
  220. self.init_after_loading_content();
  221. }, false);
  222. };
  223. Viewer.prototype = {
  224. scale : 1,
  225. /*
  226. * index of the active page (the one with largest visible area)
  227. * which estimates the page currently being viewed
  228. */
  229. cur_page_idx : 0,
  230. /*
  231. * index of the first visible page
  232. * used when determining current view
  233. */
  234. first_page_idx : 0,
  235. init_before_loading_content : function() {
  236. /* hide all pages before loading, will reveal only visible ones later */
  237. this.pre_hide_pages();
  238. },
  239. initialize_radio_button : function() {
  240. var elements = document.getElementsByClassName(CSS_CLASS_NAMES.input_radio);
  241. for(var i = 0; i < elements.length; i++) {
  242. var r = elements[i];
  243. r.addEventListener('click', function() {
  244. this.classList.toggle("checked");
  245. });
  246. }
  247. },
  248. init_after_loading_content : function() {
  249. this.sidebar = document.getElementById(this.config['sidebar_id']);
  250. this.outline = document.getElementById(this.config['outline_id']);
  251. this.container = document.getElementById(this.config['container_id']);
  252. this.loading_indicator = document.getElementsByClassName(this.config['loading_indicator_cls'])[0];
  253. {
  254. // Open the outline if nonempty
  255. var empty = true;
  256. var nodes = this.outline.childNodes;
  257. for (var i = 0, l = nodes.length; i < l; ++i) {
  258. var cur_node = nodes[i];
  259. if (cur_node.nodeName.toLowerCase() === 'ul') {
  260. empty = false;
  261. break;
  262. }
  263. }
  264. if (!empty)
  265. this.sidebar.classList.add('opened');
  266. }
  267. this.find_pages();
  268. // do nothing if there's nothing
  269. if(this.pages.length == 0) return;
  270. // disable dragging of background images
  271. disable_dragstart(document.getElementsByClassName(CSS_CLASS_NAMES.background_image));
  272. if (this.config['key_handler'])
  273. this.register_key_handler();
  274. var self = this;
  275. if (this.config['hashchange_handler']) {
  276. window.addEventListener('hashchange', function(e) {
  277. self.navigate_to_dest(document.location.hash.substring(1));
  278. }, false);
  279. }
  280. if (this.config['view_history_handler']) {
  281. window.addEventListener('popstate', function(e) {
  282. if(e.state) self.navigate_to_dest(e.state);
  283. }, false);
  284. }
  285. // register schedule rendering
  286. // renew old schedules since scroll() may be called frequently
  287. this.container.addEventListener('scroll', function() {
  288. self.update_page_idx();
  289. self.schedule_render(true);
  290. }, false);
  291. // handle links
  292. [this.container, this.outline].forEach(function(ele) {
  293. ele.addEventListener('click', self.link_handler.bind(self), false);
  294. });
  295. this.initialize_radio_button();
  296. this.render();
  297. if (parent) {
  298. var e = this;
  299. var page = e.pages.length;
  300. var original_height = [];
  301. var height = e.container.clientHeight;
  302. var width = 0;
  303. for (var i in e.pages) {
  304. height = height + e.pages[i].original_height;
  305. if (width < e.pages[i].original_width) {
  306. width = e.pages[i].original_width
  307. }
  308. original_height.push(e.pages[i].original_height);
  309. };
  310. parent.docView(width, height, original_height, page);
  311. }
  312. },
  313. /*
  314. * set up this.pages and this.page_map
  315. * pages is an array holding all the Page objects
  316. * page-Map maps an original page number (in PDF) to the corresponding index in page
  317. */
  318. find_pages : function() {
  319. var new_pages = [];
  320. var new_page_map = {};
  321. var nodes = this.container.childNodes;
  322. for (var i = 0, l = nodes.length; i < l; ++i) {
  323. var cur_node = nodes[i];
  324. if ((cur_node.nodeType === Node.ELEMENT_NODE)
  325. && cur_node.classList.contains(CSS_CLASS_NAMES.page_frame)) {
  326. var p = new Page(cur_node);
  327. new_pages.push(p);
  328. new_page_map[p.num] = new_pages.length - 1;
  329. }
  330. }
  331. this.pages = new_pages;
  332. this.page_map = new_page_map;
  333. },
  334. /**
  335. * @param{number} idx
  336. * @param{number=} pages_to_preload
  337. * @param{function(Page)=} callback
  338. *
  339. * TODO: remove callback -> promise ?
  340. */
  341. load_page : function(idx, pages_to_preload, callback) {
  342. var pages = this.pages;
  343. if (idx >= pages.length)
  344. return; // Page does not exist
  345. var cur_page = pages[idx];
  346. if (cur_page.loaded)
  347. return; // Page is loaded
  348. if (this.pages_loading[idx])
  349. return; // Page is already loading
  350. var cur_page_ele = cur_page.page;
  351. var url = cur_page_ele.getAttribute('data-page-url');
  352. if (url) {
  353. this.pages_loading[idx] = true; // set semaphore
  354. // add a copy of the loading indicator if not already present
  355. var new_loading_indicator = cur_page_ele.getElementsByClassName(this.config['loading_indicator_cls'])[0];
  356. if (typeof new_loading_indicator === 'undefined'){
  357. new_loading_indicator = this.loading_indicator.cloneNode(true);
  358. new_loading_indicator.classList.add('active');
  359. cur_page_ele.appendChild(new_loading_indicator);
  360. }
  361. // load data
  362. {
  363. var self = this;
  364. var _idx = idx;
  365. var xhr = new XMLHttpRequest();
  366. xhr.open('GET', url, true);
  367. xhr.onload = function(){
  368. if (xhr.status === 200 || xhr.status === 0) {
  369. // find the page element in the data
  370. var div = document.createElement('div');
  371. div.innerHTML = xhr.responseText;
  372. var new_page = null;
  373. var nodes = div.childNodes;
  374. for (var i = 0, l = nodes.length; i < l; ++i) {
  375. var cur_node = nodes[i];
  376. if ((cur_node.nodeType === Node.ELEMENT_NODE)
  377. && cur_node.classList.contains(CSS_CLASS_NAMES.page_frame)) {
  378. new_page = cur_node;
  379. break;
  380. }
  381. }
  382. // replace the old page with loaded data
  383. // the loading indicator on this page should also be destroyed
  384. var p = self.pages[_idx];
  385. self.container.replaceChild(new_page, p.page);
  386. p = new Page(new_page);
  387. self.pages[_idx] = p;
  388. p.hide();
  389. p.rescale(self.scale);
  390. // disable background image dragging
  391. disable_dragstart(new_page.getElementsByClassName(CSS_CLASS_NAMES.background_image));
  392. self.schedule_render(false);
  393. if (callback){ callback(p); }
  394. }
  395. // Reset loading token
  396. delete self.pages_loading[_idx];
  397. };
  398. xhr.send(null);
  399. }
  400. }
  401. // Concurrent prefetch of the next pages
  402. if (pages_to_preload === undefined)
  403. pages_to_preload = this.config['preload_pages'];
  404. if (--pages_to_preload > 0) {
  405. var self = this;
  406. setTimeout(function() {
  407. self.load_page(idx+1, pages_to_preload);
  408. },0);
  409. }
  410. },
  411. /*
  412. * Hide all pages that have no 'opened' class
  413. * The 'opened' class will be added to visible pages by JavaScript
  414. * We cannot add this in the default CSS because JavaScript may be disabled
  415. */
  416. pre_hide_pages : function() {
  417. /* pages might have not been loaded yet, so add a CSS rule */
  418. var s = '@media screen{.'+CSS_CLASS_NAMES.page_content_box+'{display:none;}}';
  419. var n = document.createElement('style');
  420. if (n.styleSheet) {
  421. n.styleSheet.cssText = s;
  422. } else {
  423. n.appendChild(document.createTextNode(s));
  424. }
  425. document.head.appendChild(n);
  426. },
  427. /*
  428. * show visible pages and hide invisible pages
  429. */
  430. render : function () {
  431. var container = this.container;
  432. /*
  433. * show the pages that are 'nearly' visible -- it's right above or below the container
  434. *
  435. * all the y values are in the all-page element's coordinate system
  436. */
  437. var container_min_y = container.scrollTop;
  438. var container_height = container.clientHeight;
  439. var container_max_y = container_min_y + container_height;
  440. var visible_min_y = container_min_y - container_height;
  441. var visible_max_y = container_max_y + container_height;
  442. var cur_page_fully_visible = false;
  443. var cur_page_idx = this.cur_page_idx;
  444. var max_visible_page_idx = cur_page_idx;
  445. var max_visible_ratio = 0.0;
  446. var pl = this.pages;
  447. for (var i = 0, l = pl.length; i < l; ++i) {
  448. var cur_page = pl[i];
  449. var cur_page_ele = cur_page.page;
  450. var page_min_y = cur_page_ele.offsetTop + cur_page_ele.clientTop;
  451. var page_height = cur_page_ele.clientHeight;
  452. var page_max_y = page_min_y + page_height;
  453. if ((page_min_y <= visible_max_y) && (page_max_y >= visible_min_y))
  454. {
  455. // cur_page is 'nearly' visible, show it or load it
  456. if (cur_page.loaded) {
  457. cur_page.show();
  458. } else {
  459. this.load_page(i);
  460. }
  461. } else {
  462. cur_page.hide();
  463. }
  464. }
  465. },
  466. /*
  467. * update cur_page_idx and first_page_idx
  468. * normally called upon scrolling
  469. */
  470. update_page_idx: function () {
  471. var pages = this.pages;
  472. var pages_len = pages.length;
  473. // there is no chance that cur_page_idx or first_page_idx is modified
  474. if (pages_len < 2) return;
  475. var container = this.container;
  476. var container_min_y = container.scrollTop;
  477. var container_max_y = container_min_y + container.clientHeight;
  478. // binary search for the first page
  479. // whose bottom border is below the top border of the container
  480. var first_idx = -1;
  481. var last_idx = pages_len;
  482. var rest_len = last_idx - first_idx;
  483. // TODO: use current first_page_idx as a hint?
  484. while(rest_len > 1) {
  485. var idx = first_idx + Math.floor(rest_len / 2);
  486. var cur_page_ele = pages[idx].page;
  487. if (cur_page_ele.offsetTop + cur_page_ele.clientTop + cur_page_ele.clientHeight >= container_min_y) {
  488. last_idx = idx;
  489. } else {
  490. first_idx = idx;
  491. }
  492. rest_len = last_idx - first_idx;
  493. }
  494. /*
  495. * with malformed settings it is possible that no page is visible, e.g.
  496. * - the container is to thin, which lies in the margin between two pages
  497. * - all pages are completely above or below the container
  498. * but we just assume that they won't happen.
  499. */
  500. this.first_page_idx = last_idx;
  501. // find the page with largest visible area
  502. var cur_page_idx = this.cur_page_idx;
  503. var max_visible_page_idx = cur_page_idx;
  504. var max_visible_ratio = 0.0;
  505. for(var i = last_idx; i < pages_len; ++i) {
  506. var cur_page_ele = pages[i].page;
  507. var page_min_y = cur_page_ele.offsetTop + cur_page_ele.clientTop;
  508. var page_height = cur_page_ele.clientHeight;
  509. var page_max_y = page_min_y + page_height;
  510. if (page_min_y > container_max_y) break;
  511. // check the visible fraction of the page
  512. var page_visible_ratio = ( Math.min(container_max_y, page_max_y)
  513. - Math.max(container_min_y, page_min_y)
  514. ) / page_height;
  515. // stay with the current page if it is still fully visible
  516. if ((i === cur_page_idx) && (Math.abs(page_visible_ratio - 1.0) <= EPS)) {
  517. max_visible_page_idx = cur_page_idx;
  518. break;
  519. }
  520. if (page_visible_ratio > max_visible_ratio) {
  521. max_visible_ratio = page_visible_ratio;
  522. max_visible_page_idx = i;
  523. }
  524. }
  525. this.cur_page_idx = max_visible_page_idx;
  526. },
  527. /**
  528. * @param{boolean} renew renew the existing schedule instead of using the old one
  529. */
  530. schedule_render : function(renew) {
  531. if (this.render_timer !== undefined) {
  532. if (!renew) return;
  533. clearTimeout(this.render_timer);
  534. }
  535. var self = this;
  536. this.render_timer = setTimeout(function () {
  537. /*
  538. * render() may trigger load_page(), which may in turn trigger another render()
  539. * so delete render_timer first
  540. */
  541. delete self.render_timer;
  542. self.render();
  543. }, this.config['render_timeout']);
  544. },
  545. /*
  546. * Handling key events, zooming, scrolling etc.
  547. */
  548. register_key_handler: function () {
  549. /*
  550. * When user try to zoom in/out using ctrl + +/- or mouse wheel
  551. * handle this and prevent the default behaviours
  552. *
  553. * Code credit to PDF.js
  554. */
  555. var self = this;
  556. // Firefox specific event, so that we can prevent browser from zooming
  557. window.addEventListener('DOMMouseScroll', function(e) {
  558. if (e.ctrlKey) {
  559. e.preventDefault();
  560. var container = self.container;
  561. var rect = container.getBoundingClientRect();
  562. var fixed_point = [e.clientX - rect['left'] - container.clientLeft
  563. ,e.clientY - rect['top'] - container.clientTop];
  564. self.rescale(Math.pow(self.config['scale_step'], e.detail), true, fixed_point);
  565. }
  566. }, false);
  567. window.addEventListener('keydown', function(e) {
  568. var handled = false;
  569. /*
  570. var cmd = (e.ctrlKey ? 1 : 0)
  571. | (e.altKey ? 2 : 0)
  572. | (e.shiftKey ? 4 : 0)
  573. | (e.metaKey ? 8 : 0)
  574. ;
  575. */
  576. var with_ctrl = e.ctrlKey || e.metaKey;
  577. var with_alt = e.altKey;
  578. switch (e.keyCode) {
  579. case 61: // FF/Mac '='
  580. case 107: // FF '+' and '='
  581. case 187: // Chrome '+'
  582. if (with_ctrl){
  583. self.rescale(1.0 / self.config['scale_step'], true);
  584. handled = true;
  585. }
  586. break;
  587. case 173: // FF/Mac '-'
  588. case 109: // FF '-'
  589. case 189: // Chrome '-'
  590. if (with_ctrl){
  591. self.rescale(self.config['scale_step'], true);
  592. handled = true;
  593. }
  594. break;
  595. case 48: // '0'
  596. if (with_ctrl){
  597. self.rescale(0, false);
  598. handled = true;
  599. }
  600. break;
  601. case 33: // Page UP:
  602. if (with_alt) { // alt-pageup -> scroll one page up
  603. self.scroll_to(self.cur_page_idx - 1);
  604. } else { // pageup -> scroll one screen up
  605. self.container.scrollTop -= self.container.clientHeight;
  606. }
  607. handled = true;
  608. break;
  609. case 34: // Page DOWN
  610. if (with_alt) { // alt-pagedown -> scroll one page down
  611. self.scroll_to(self.cur_page_idx + 1);
  612. } else { // pagedown -> scroll one screen down
  613. self.container.scrollTop += self.container.clientHeight;
  614. }
  615. handled = true;
  616. break;
  617. case 35: // End
  618. self.container.scrollTop = self.container.scrollHeight;
  619. handled = true;
  620. break;
  621. case 36: // Home
  622. self.container.scrollTop = 0;
  623. handled = true;
  624. break;
  625. }
  626. if (handled) {
  627. e.preventDefault();
  628. return;
  629. }
  630. }, false);
  631. },
  632. /**
  633. * @param{number} ratio
  634. * @param{boolean} is_relative
  635. * @param{Array.<number>=} fixed_point preserve the position (relative to the top-left corner of the viewer) after rescaling
  636. */
  637. rescale : function (ratio, is_relative, fixed_point) {
  638. var old_scale = this.scale;
  639. var new_scale = old_scale;
  640. // set new scale
  641. if (ratio === 0) {
  642. new_scale = 1;
  643. is_relative = false;
  644. } else if (is_relative)
  645. new_scale *= ratio;
  646. else
  647. new_scale = ratio;
  648. this.scale = new_scale;
  649. if (!fixed_point)
  650. fixed_point = [0,0];
  651. // translate fixed_point to the coordinate system of all pages
  652. var container = this.container;
  653. fixed_point[0] += container.scrollLeft;
  654. fixed_point[1] += container.scrollTop;
  655. // find the visible page that contains the fixed point
  656. // if the fixed point lies between two pages (including their borders), it's contained in the first one
  657. var pl = this.pages;
  658. var pl_len = pl.length;
  659. for (var i = this.first_page_idx; i < pl_len; ++i) {
  660. var p = pl[i].page;
  661. if (p.offsetTop + p.clientTop >= fixed_point[1])
  662. break;
  663. }
  664. var fixed_point_page_idx = i - 1;
  665. // determine the new scroll position
  666. // each-value consists of two parts, one inside the page, which is affected by rescaling,
  667. // the other is outside, (e.g. borders and margins), which is not affected
  668. // if the fixed_point is above the first page, use the first page as the reference
  669. if (fixed_point_page_idx < 0)
  670. fixed_point_page_idx = 0;
  671. var fp_p = pl[fixed_point_page_idx].page;
  672. var fp_p_width = fp_p.clientWidth;
  673. var fp_p_height = fp_p.clientHeight;
  674. var fp_x_ref = fp_p.offsetLeft + fp_p.clientLeft;
  675. var fp_x_inside = fixed_point[0] - fp_x_ref;
  676. if (fp_x_inside < 0)
  677. fp_x_inside = 0;
  678. else if (fp_x_inside > fp_p_width)
  679. fp_x_inside = fp_p_width;
  680. var fp_y_ref = fp_p.offsetTop + fp_p.clientTop;
  681. var fp_y_inside = fixed_point[1] - fp_y_ref;
  682. if (fp_y_inside < 0)
  683. fp_y_inside = 0;
  684. else if (fp_y_inside > fp_p_height)
  685. fp_y_inside = fp_p_height;
  686. // Rescale pages
  687. for (var i = 0; i < pl_len; ++i)
  688. pl[i].rescale(new_scale);
  689. // Correct container scroll to keep view aligned while zooming
  690. container.scrollLeft += fp_x_inside / old_scale * new_scale + fp_p.offsetLeft + fp_p.clientLeft - fp_x_inside - fp_x_ref;
  691. container.scrollTop += fp_y_inside / old_scale * new_scale + fp_p.offsetTop + fp_p.clientTop - fp_y_inside - fp_y_ref;
  692. // some pages' visibility may be toggled, wait for next render()
  693. // renew old schedules since rescale() may be called frequently
  694. this.schedule_render(true);
  695. },
  696. fit_width : function () {
  697. var page_idx = this.cur_page_idx;
  698. this.rescale(this.container.clientWidth / this.pages[page_idx].width(), true);
  699. this.scroll_to(page_idx);
  700. },
  701. fit_height : function () {
  702. var page_idx = this.cur_page_idx;
  703. this.rescale(this.container.clientHeight / this.pages[page_idx].height(), true);
  704. this.scroll_to(page_idx);
  705. },
  706. /**
  707. * @param{Node} ele
  708. */
  709. get_containing_page : function(ele) {
  710. /* get the page obj containing obj */
  711. while(ele) {
  712. if ((ele.nodeType === Node.ELEMENT_NODE)
  713. && ele.classList.contains(CSS_CLASS_NAMES.page_frame)) {
  714. /*
  715. * Get original page number and map it to index of pages
  716. * TODO: store the index on the dom element
  717. */
  718. var pn = get_page_number(/** @type{Element} */(ele));
  719. var pm = this.page_map;
  720. return (pn in pm) ? this.pages[pm[pn]] : null;
  721. }
  722. ele = ele.parentNode;
  723. }
  724. return null;
  725. },
  726. /**
  727. * @param{Event} e
  728. */
  729. link_handler : function (e) {
  730. var target = /** @type{Node} */(e.target);
  731. var detail_str = /** @type{string} */ (target.getAttribute('data-dest-detail'));
  732. if (!detail_str) return;
  733. if (this.config['view_history_handler']) {
  734. try {
  735. var cur_hash = this.get_current_view_hash();
  736. window.history.replaceState(cur_hash, '', '#' + cur_hash);
  737. window.history.pushState(detail_str, '', '#' + detail_str);
  738. } catch(ex) { }
  739. }
  740. this.navigate_to_dest(detail_str, this.get_containing_page(target));
  741. e.preventDefault();
  742. },
  743. /**
  744. * @param{string} detail_str may come from user provided hashtag, need sanitizing
  745. * @param{Page=} src_page page containing the source event (e.g. link)
  746. */
  747. navigate_to_dest : function(detail_str, src_page) {
  748. try {
  749. var detail = JSON.parse(detail_str);
  750. } catch(e) {
  751. return;
  752. }
  753. if(!(detail instanceof Array)) return;
  754. var target_page_no = detail[0];
  755. var page_map = this.page_map;
  756. if (!(target_page_no in page_map)) return;
  757. var target_page_idx = page_map[target_page_no];
  758. var target_page = this.pages[target_page_idx];
  759. for (var i = 2, l = detail.length; i < l; ++i) {
  760. var d = detail[i];
  761. if(!((d === null) || (typeof d === 'number')))
  762. return;
  763. }
  764. while(detail.length < 6)
  765. detail.push(null);
  766. // cur_page might be undefined, e.g. from Outline
  767. var cur_page = src_page || this.pages[this.cur_page_idx];
  768. var cur_pos = cur_page.view_position();
  769. cur_pos = transform(cur_page.ictm, [cur_pos[0], cur_page.height()-cur_pos[1]]);
  770. var zoom = this.scale;
  771. var pos = [0,0];
  772. var upside_down = true;
  773. var ok = false;
  774. // position specified in `detail` are in the raw coordinate system of the page (unscaled)
  775. var scale = this.scale;
  776. // TODO: fitb*
  777. // TODO: BBox
  778. switch(detail[1]) {
  779. case 'XYZ':
  780. pos = [ (detail[2] === null) ? cur_pos[0] : detail[2] * scale
  781. , (detail[3] === null) ? cur_pos[1] : detail[3] * scale ];
  782. zoom = detail[4];
  783. if ((zoom === null) || (zoom === 0))
  784. zoom = this.scale;
  785. ok = true;
  786. break;
  787. case 'Fit':
  788. case 'FitB':
  789. pos = [0,0];
  790. ok = true;
  791. break;
  792. case 'FitH':
  793. case 'FitBH':
  794. pos = [0, (detail[2] === null) ? cur_pos[1] : detail[2] * scale];
  795. ok = true;
  796. break;
  797. case 'FitV':
  798. case 'FitBV':
  799. pos = [(detail[2] === null) ? cur_pos[0] : detail[2] * scale, 0];
  800. ok = true;
  801. break;
  802. case 'FitR':
  803. /* locate the top-left corner of the rectangle */
  804. // TODO
  805. pos = [detail[2] * scale, detail[5] * scale];
  806. upside_down = false;
  807. ok = true;
  808. break;
  809. default:
  810. break;
  811. }
  812. if (!ok) return;
  813. this.rescale(zoom, false);
  814. var self = this;
  815. /**
  816. * page should have type Page
  817. * @param{Page} page
  818. */
  819. var transform_and_scroll = function(page) {
  820. pos = transform(page.ctm, pos);
  821. if (upside_down) {
  822. pos[1] = page.height() - pos[1];
  823. }
  824. self.scroll_to(target_page_idx, pos);
  825. };
  826. if (target_page.loaded) {
  827. transform_and_scroll(target_page);
  828. } else {
  829. // TODO: scroll_to may finish before load_page
  830. // Scroll to the exact position once loaded.
  831. this.load_page(target_page_idx, undefined, transform_and_scroll);
  832. // In the meantime page gets loaded, scroll approximately position for maximum responsiveness.
  833. this.scroll_to(target_page_idx);
  834. }
  835. },
  836. /**
  837. * @param{number} page_idx
  838. * @param{Array.<number>=} pos [x,y] where (0,0) is the top-left corner
  839. */
  840. scroll_to : function(page_idx, pos) {
  841. var pl = this.pages;
  842. if ((page_idx < 0) || (page_idx >= pl.length)) return;
  843. var target_page = pl[page_idx];
  844. var cur_target_pos = target_page.view_position();
  845. if (pos === undefined)
  846. pos = [0,0];
  847. var container = this.container;
  848. container.scrollLeft += pos[0] - cur_target_pos[0];
  849. container.scrollTop += pos[1] - cur_target_pos[1];
  850. },
  851. /**
  852. * generate the hash for the current view
  853. */
  854. get_current_view_hash : function() {
  855. var detail = [];
  856. var cur_page = this.pages[this.cur_page_idx];
  857. detail.push(cur_page.num);
  858. detail.push('XYZ');
  859. var cur_pos = cur_page.view_position();
  860. cur_pos = transform(cur_page.ictm, [cur_pos[0], cur_page.height()-cur_pos[1]]);
  861. detail.push(cur_pos[0] / this.scale);
  862. detail.push(cur_pos[1] / this.scale);
  863. detail.push(this.scale);
  864. return JSON.stringify(detail);
  865. }
  866. };
  867. // export pdf2htmlEX.Viewer
  868. pdf2htmlEX['Viewer'] = Viewer;