/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab filetype=javascript : */ /** * @license pdf2htmlEX.js: Core UI functions for pdf2htmlEX * Copyright 2012,2013 Lu Wang and other contributors * https://github.com/coolwanglu/pdf2htmlEX/blob/master/share/LICENSE */ /* * Attention: * This files is to be optimized by closure-compiler, * so pay attention to the forms of property names: * * string/bracket form is safe, won't be optimized: * var obj={ 'a':'b' }; obj['a'] = 'b'; * name/dot form will be optimized, the name is likely to be modified: * var obj={ a:'b' }; obj.a = 'b'; * * Either form can be used for internal objects, * but must be consistent for each one respectively. * * string/bracket form must be used for external objects * e.g. DEFAULT_CONFIG, object stored in page-data * property names are part of the `protocol` in these cases. * */ 'use strict'; var pdf2htmlEX = window['pdf2htmlEX'] = window['pdf2htmlEX'] || {}; /** * @const * @struct */ var CSS_CLASS_NAMES = { page_frame : 'pf', page_content_box : 'pc', page_data : 'pi', background_image : 'bi', link : 'l', input_radio : 'ir', __dummy__ : 'no comma' }; /** * configurations of Viewer * @const * @dict */ var DEFAULT_CONFIG = { // id of the element to put the pages in 'container_id' : 'page-container', // id of the element for sidebar (to open and close) 'sidebar_id' : 'sidebar', // id of the element for outline 'outline_id' : 'outline', // class for the loading indicator 'loading_indicator_cls' : 'loading-indicator', // How many page shall we preload that are below the last visible page 'preload_pages' : 3, // how many ms should we wait before actually rendering the pages and after a scroll event 'render_timeout' : 100, // zoom ratio step for each zoom in/out event 'scale_step' : 0.9, // register global key handler, allowing navigation by keyboard 'key_handler' : true, // register hashchange handler, navigate to the location specified by the hash 'hashchange_handler' : true, // register view history handler, allowing going back to the previous location 'view_history_handler' : true, '__dummy__' : 'no comma' }; /** @const */ var EPS = 1e-6; /************************************/ /* utility function */ /** * @param{Array.} ctm */ function invert(ctm) { var det = ctm[0] * ctm[3] - ctm[1] * ctm[2]; return [ ctm[3] / det ,-ctm[1] / det ,-ctm[2] / det ,ctm[0] / det ,(ctm[2] * ctm[5] - ctm[3] * ctm[4]) / det ,(ctm[1] * ctm[4] - ctm[0] * ctm[5]) / det ]; }; /** * @param{Array.} ctm * @param{Array.} pos */ function transform(ctm, pos) { return [ctm[0] * pos[0] + ctm[2] * pos[1] + ctm[4] ,ctm[1] * pos[0] + ctm[3] * pos[1] + ctm[5]]; }; /** * @param{Element} ele */ function get_page_number(ele) { return parseInt(ele.getAttribute('data-page-no'), 16); }; /** * @param{NodeList} eles */ function disable_dragstart(eles) { for (var i = 0, l = eles.length; i < l; ++i) { eles[i].addEventListener('dragstart', function() { return false; }, false); } }; /** * @param{...Object} var_args */ function clone_and_extend_objs(var_args) { var result_obj = {}; for (var i = 0, l = arguments.length; i < l; ++i) { var cur_obj = arguments[i]; for (var k in cur_obj) { if (cur_obj.hasOwnProperty(k)) { result_obj[k] = cur_obj[k]; } } } return result_obj; }; /** * @constructor * @param{Element} page The element for the page */ function Page(page) { if (!page) return; this.loaded = false; this.shown = false; this.page = page; // page frame element this.num = get_page_number(page); // page size // Need to make rescale work when page_content_box is not loaded, yet this.original_height = page.clientHeight; this.original_width = page.clientWidth; // content box var content_box = page.getElementsByClassName(CSS_CLASS_NAMES.page_content_box)[0]; // if page is loaded if (content_box) { this.content_box = content_box; /* * scale ratios * * original_scale : the first one * cur_scale : currently using */ this.original_scale = this.cur_scale = this.original_height / content_box.clientHeight; this.page_data = JSON.parse(page.getElementsByClassName(CSS_CLASS_NAMES.page_data)[0].getAttribute('data-data')); this.ctm = this.page_data['ctm']; this.ictm = invert(this.ctm); this.loaded = true; } }; Page.prototype = { /* hide & show are for contents, the page frame is still there */ hide : function(){ if (this.loaded && this.shown) { this.content_box.classList.remove('opened'); this.shown = false; } }, show : function(){ if (this.loaded && !this.shown) { this.content_box.classList.add('opened'); this.shown = true; } }, /** * @param{number} ratio */ rescale : function(ratio) { if (ratio === 0) { // reset scale this.cur_scale = this.original_scale; } else { this.cur_scale = ratio; } // scale the content box if (this.loaded) { var cbs = this.content_box.style; cbs.msTransform = cbs.webkitTransform = cbs.transform = 'scale('+this.cur_scale.toFixed(3)+')'; } // stretch the page frame to hold the place { var ps = this.page.style; ps.height = (this.original_height * this.cur_scale) + 'px'; ps.width = (this.original_width * this.cur_scale) + 'px'; } }, /* * return the coordinate of the top-left corner of container * in our coordinate system * assuming that p.parentNode === p.offsetParent */ view_position : function () { var p = this.page; var c = p.parentNode; return [c.scrollLeft - p.offsetLeft - p.clientLeft ,c.scrollTop - p.offsetTop - p.clientTop]; }, height : function () { return this.page.clientHeight; }, width : function () { return this.page.clientWidth; } }; /** * @constructor * @param{Object=} config */ function Viewer(config) { this.config = clone_and_extend_objs(DEFAULT_CONFIG, (arguments.length > 0 ? config : {})); this.pages_loading = []; this.init_before_loading_content(); var self = this; document.addEventListener('DOMContentLoaded', function(){ self.init_after_loading_content(); }, false); }; Viewer.prototype = { scale : 1, /* * index of the active page (the one with largest visible area) * which estimates the page currently being viewed */ cur_page_idx : 0, /* * index of the first visible page * used when determining current view */ first_page_idx : 0, init_before_loading_content : function() { /* hide all pages before loading, will reveal only visible ones later */ this.pre_hide_pages(); }, initialize_radio_button : function() { var elements = document.getElementsByClassName(CSS_CLASS_NAMES.input_radio); for(var i = 0; i < elements.length; i++) { var r = elements[i]; r.addEventListener('click', function() { this.classList.toggle("checked"); }); } }, init_after_loading_content : function() { this.sidebar = document.getElementById(this.config['sidebar_id']); this.outline = document.getElementById(this.config['outline_id']); this.container = document.getElementById(this.config['container_id']); this.loading_indicator = document.getElementsByClassName(this.config['loading_indicator_cls'])[0]; { // Open the outline if nonempty var empty = true; var nodes = this.outline.childNodes; for (var i = 0, l = nodes.length; i < l; ++i) { var cur_node = nodes[i]; if (cur_node.nodeName.toLowerCase() === 'ul') { empty = false; break; } } if (!empty) this.sidebar.classList.add('opened'); } this.find_pages(); // do nothing if there's nothing if(this.pages.length == 0) return; // disable dragging of background images disable_dragstart(document.getElementsByClassName(CSS_CLASS_NAMES.background_image)); if (this.config['key_handler']) this.register_key_handler(); var self = this; if (this.config['hashchange_handler']) { window.addEventListener('hashchange', function(e) { self.navigate_to_dest(document.location.hash.substring(1)); }, false); } if (this.config['view_history_handler']) { window.addEventListener('popstate', function(e) { if(e.state) self.navigate_to_dest(e.state); }, false); } // register schedule rendering // renew old schedules since scroll() may be called frequently this.container.addEventListener('scroll', function() { self.update_page_idx(); self.schedule_render(true); }, false); // handle links [this.container, this.outline].forEach(function(ele) { ele.addEventListener('click', self.link_handler.bind(self), false); }); this.initialize_radio_button(); this.render(); if (parent) { var e = this; var page = e.pages.length; var original_height = []; var height = e.container.clientHeight; var width = 0; for (var i in e.pages) { height = height + e.pages[i].original_height; if (width < e.pages[i].original_width) { width = e.pages[i].original_width } original_height.push(e.pages[i].original_height); }; parent.docView(width, height, original_height, page); } }, /* * set up this.pages and this.page_map * pages is an array holding all the Page objects * page-Map maps an original page number (in PDF) to the corresponding index in page */ find_pages : function() { var new_pages = []; var new_page_map = {}; var nodes = this.container.childNodes; for (var i = 0, l = nodes.length; i < l; ++i) { var cur_node = nodes[i]; if ((cur_node.nodeType === Node.ELEMENT_NODE) && cur_node.classList.contains(CSS_CLASS_NAMES.page_frame)) { var p = new Page(cur_node); new_pages.push(p); new_page_map[p.num] = new_pages.length - 1; } } this.pages = new_pages; this.page_map = new_page_map; }, /** * @param{number} idx * @param{number=} pages_to_preload * @param{function(Page)=} callback * * TODO: remove callback -> promise ? */ load_page : function(idx, pages_to_preload, callback) { var pages = this.pages; if (idx >= pages.length) return; // Page does not exist var cur_page = pages[idx]; if (cur_page.loaded) return; // Page is loaded if (this.pages_loading[idx]) return; // Page is already loading var cur_page_ele = cur_page.page; var url = cur_page_ele.getAttribute('data-page-url'); if (url) { this.pages_loading[idx] = true; // set semaphore // add a copy of the loading indicator if not already present var new_loading_indicator = cur_page_ele.getElementsByClassName(this.config['loading_indicator_cls'])[0]; if (typeof new_loading_indicator === 'undefined'){ new_loading_indicator = this.loading_indicator.cloneNode(true); new_loading_indicator.classList.add('active'); cur_page_ele.appendChild(new_loading_indicator); } // load data { var self = this; var _idx = idx; var xhr = new XMLHttpRequest(); xhr.open('GET', url, true); xhr.onload = function(){ if (xhr.status === 200 || xhr.status === 0) { // find the page element in the data var div = document.createElement('div'); div.innerHTML = xhr.responseText; var new_page = null; var nodes = div.childNodes; for (var i = 0, l = nodes.length; i < l; ++i) { var cur_node = nodes[i]; if ((cur_node.nodeType === Node.ELEMENT_NODE) && cur_node.classList.contains(CSS_CLASS_NAMES.page_frame)) { new_page = cur_node; break; } } // replace the old page with loaded data // the loading indicator on this page should also be destroyed var p = self.pages[_idx]; self.container.replaceChild(new_page, p.page); p = new Page(new_page); self.pages[_idx] = p; p.hide(); p.rescale(self.scale); // disable background image dragging disable_dragstart(new_page.getElementsByClassName(CSS_CLASS_NAMES.background_image)); self.schedule_render(false); if (callback){ callback(p); } } // Reset loading token delete self.pages_loading[_idx]; }; xhr.send(null); } } // Concurrent prefetch of the next pages if (pages_to_preload === undefined) pages_to_preload = this.config['preload_pages']; if (--pages_to_preload > 0) { var self = this; setTimeout(function() { self.load_page(idx+1, pages_to_preload); },0); } }, /* * Hide all pages that have no 'opened' class * The 'opened' class will be added to visible pages by JavaScript * We cannot add this in the default CSS because JavaScript may be disabled */ pre_hide_pages : function() { /* pages might have not been loaded yet, so add a CSS rule */ var s = '@media screen{.'+CSS_CLASS_NAMES.page_content_box+'{display:none;}}'; var n = document.createElement('style'); if (n.styleSheet) { n.styleSheet.cssText = s; } else { n.appendChild(document.createTextNode(s)); } document.head.appendChild(n); }, /* * show visible pages and hide invisible pages */ render : function () { var container = this.container; /* * show the pages that are 'nearly' visible -- it's right above or below the container * * all the y values are in the all-page element's coordinate system */ var container_min_y = container.scrollTop; var container_height = container.clientHeight; var container_max_y = container_min_y + container_height; var visible_min_y = container_min_y - container_height; var visible_max_y = container_max_y + container_height; var cur_page_fully_visible = false; var cur_page_idx = this.cur_page_idx; var max_visible_page_idx = cur_page_idx; var max_visible_ratio = 0.0; var pl = this.pages; for (var i = 0, l = pl.length; i < l; ++i) { var cur_page = pl[i]; var cur_page_ele = cur_page.page; var page_min_y = cur_page_ele.offsetTop + cur_page_ele.clientTop; var page_height = cur_page_ele.clientHeight; var page_max_y = page_min_y + page_height; if ((page_min_y <= visible_max_y) && (page_max_y >= visible_min_y)) { // cur_page is 'nearly' visible, show it or load it if (cur_page.loaded) { cur_page.show(); } else { this.load_page(i); } } else { cur_page.hide(); } } }, /* * update cur_page_idx and first_page_idx * normally called upon scrolling */ update_page_idx: function () { var pages = this.pages; var pages_len = pages.length; // there is no chance that cur_page_idx or first_page_idx is modified if (pages_len < 2) return; var container = this.container; var container_min_y = container.scrollTop; var container_max_y = container_min_y + container.clientHeight; // binary search for the first page // whose bottom border is below the top border of the container var first_idx = -1; var last_idx = pages_len; var rest_len = last_idx - first_idx; // TODO: use current first_page_idx as a hint? while(rest_len > 1) { var idx = first_idx + Math.floor(rest_len / 2); var cur_page_ele = pages[idx].page; if (cur_page_ele.offsetTop + cur_page_ele.clientTop + cur_page_ele.clientHeight >= container_min_y) { last_idx = idx; } else { first_idx = idx; } rest_len = last_idx - first_idx; } /* * with malformed settings it is possible that no page is visible, e.g. * - the container is to thin, which lies in the margin between two pages * - all pages are completely above or below the container * but we just assume that they won't happen. */ this.first_page_idx = last_idx; // find the page with largest visible area var cur_page_idx = this.cur_page_idx; var max_visible_page_idx = cur_page_idx; var max_visible_ratio = 0.0; for(var i = last_idx; i < pages_len; ++i) { var cur_page_ele = pages[i].page; var page_min_y = cur_page_ele.offsetTop + cur_page_ele.clientTop; var page_height = cur_page_ele.clientHeight; var page_max_y = page_min_y + page_height; if (page_min_y > container_max_y) break; // check the visible fraction of the page var page_visible_ratio = ( Math.min(container_max_y, page_max_y) - Math.max(container_min_y, page_min_y) ) / page_height; // stay with the current page if it is still fully visible if ((i === cur_page_idx) && (Math.abs(page_visible_ratio - 1.0) <= EPS)) { max_visible_page_idx = cur_page_idx; break; } if (page_visible_ratio > max_visible_ratio) { max_visible_ratio = page_visible_ratio; max_visible_page_idx = i; } } this.cur_page_idx = max_visible_page_idx; }, /** * @param{boolean} renew renew the existing schedule instead of using the old one */ schedule_render : function(renew) { if (this.render_timer !== undefined) { if (!renew) return; clearTimeout(this.render_timer); } var self = this; this.render_timer = setTimeout(function () { /* * render() may trigger load_page(), which may in turn trigger another render() * so delete render_timer first */ delete self.render_timer; self.render(); }, this.config['render_timeout']); }, /* * Handling key events, zooming, scrolling etc. */ register_key_handler: function () { /* * When user try to zoom in/out using ctrl + +/- or mouse wheel * handle this and prevent the default behaviours * * Code credit to PDF.js */ var self = this; // Firefox specific event, so that we can prevent browser from zooming window.addEventListener('DOMMouseScroll', function(e) { if (e.ctrlKey) { e.preventDefault(); var container = self.container; var rect = container.getBoundingClientRect(); var fixed_point = [e.clientX - rect['left'] - container.clientLeft ,e.clientY - rect['top'] - container.clientTop]; self.rescale(Math.pow(self.config['scale_step'], e.detail), true, fixed_point); } }, false); window.addEventListener('keydown', function(e) { var handled = false; /* var cmd = (e.ctrlKey ? 1 : 0) | (e.altKey ? 2 : 0) | (e.shiftKey ? 4 : 0) | (e.metaKey ? 8 : 0) ; */ var with_ctrl = e.ctrlKey || e.metaKey; var with_alt = e.altKey; switch (e.keyCode) { case 61: // FF/Mac '=' case 107: // FF '+' and '=' case 187: // Chrome '+' if (with_ctrl){ self.rescale(1.0 / self.config['scale_step'], true); handled = true; } break; case 173: // FF/Mac '-' case 109: // FF '-' case 189: // Chrome '-' if (with_ctrl){ self.rescale(self.config['scale_step'], true); handled = true; } break; case 48: // '0' if (with_ctrl){ self.rescale(0, false); handled = true; } break; case 33: // Page UP: if (with_alt) { // alt-pageup -> scroll one page up self.scroll_to(self.cur_page_idx - 1); } else { // pageup -> scroll one screen up self.container.scrollTop -= self.container.clientHeight; } handled = true; break; case 34: // Page DOWN if (with_alt) { // alt-pagedown -> scroll one page down self.scroll_to(self.cur_page_idx + 1); } else { // pagedown -> scroll one screen down self.container.scrollTop += self.container.clientHeight; } handled = true; break; case 35: // End self.container.scrollTop = self.container.scrollHeight; handled = true; break; case 36: // Home self.container.scrollTop = 0; handled = true; break; } if (handled) { e.preventDefault(); return; } }, false); }, /** * @param{number} ratio * @param{boolean} is_relative * @param{Array.=} fixed_point preserve the position (relative to the top-left corner of the viewer) after rescaling */ rescale : function (ratio, is_relative, fixed_point) { var old_scale = this.scale; var new_scale = old_scale; // set new scale if (ratio === 0) { new_scale = 1; is_relative = false; } else if (is_relative) new_scale *= ratio; else new_scale = ratio; this.scale = new_scale; if (!fixed_point) fixed_point = [0,0]; // translate fixed_point to the coordinate system of all pages var container = this.container; fixed_point[0] += container.scrollLeft; fixed_point[1] += container.scrollTop; // find the visible page that contains the fixed point // if the fixed point lies between two pages (including their borders), it's contained in the first one var pl = this.pages; var pl_len = pl.length; for (var i = this.first_page_idx; i < pl_len; ++i) { var p = pl[i].page; if (p.offsetTop + p.clientTop >= fixed_point[1]) break; } var fixed_point_page_idx = i - 1; // determine the new scroll position // each-value consists of two parts, one inside the page, which is affected by rescaling, // the other is outside, (e.g. borders and margins), which is not affected // if the fixed_point is above the first page, use the first page as the reference if (fixed_point_page_idx < 0) fixed_point_page_idx = 0; var fp_p = pl[fixed_point_page_idx].page; var fp_p_width = fp_p.clientWidth; var fp_p_height = fp_p.clientHeight; var fp_x_ref = fp_p.offsetLeft + fp_p.clientLeft; var fp_x_inside = fixed_point[0] - fp_x_ref; if (fp_x_inside < 0) fp_x_inside = 0; else if (fp_x_inside > fp_p_width) fp_x_inside = fp_p_width; var fp_y_ref = fp_p.offsetTop + fp_p.clientTop; var fp_y_inside = fixed_point[1] - fp_y_ref; if (fp_y_inside < 0) fp_y_inside = 0; else if (fp_y_inside > fp_p_height) fp_y_inside = fp_p_height; // Rescale pages for (var i = 0; i < pl_len; ++i) pl[i].rescale(new_scale); // Correct container scroll to keep view aligned while zooming container.scrollLeft += fp_x_inside / old_scale * new_scale + fp_p.offsetLeft + fp_p.clientLeft - fp_x_inside - fp_x_ref; container.scrollTop += fp_y_inside / old_scale * new_scale + fp_p.offsetTop + fp_p.clientTop - fp_y_inside - fp_y_ref; // some pages' visibility may be toggled, wait for next render() // renew old schedules since rescale() may be called frequently this.schedule_render(true); }, fit_width : function () { var page_idx = this.cur_page_idx; this.rescale(this.container.clientWidth / this.pages[page_idx].width(), true); this.scroll_to(page_idx); }, fit_height : function () { var page_idx = this.cur_page_idx; this.rescale(this.container.clientHeight / this.pages[page_idx].height(), true); this.scroll_to(page_idx); }, /** * @param{Node} ele */ get_containing_page : function(ele) { /* get the page obj containing obj */ while(ele) { if ((ele.nodeType === Node.ELEMENT_NODE) && ele.classList.contains(CSS_CLASS_NAMES.page_frame)) { /* * Get original page number and map it to index of pages * TODO: store the index on the dom element */ var pn = get_page_number(/** @type{Element} */(ele)); var pm = this.page_map; return (pn in pm) ? this.pages[pm[pn]] : null; } ele = ele.parentNode; } return null; }, /** * @param{Event} e */ link_handler : function (e) { var target = /** @type{Node} */(e.target); var detail_str = /** @type{string} */ (target.getAttribute('data-dest-detail')); if (!detail_str) return; if (this.config['view_history_handler']) { try { var cur_hash = this.get_current_view_hash(); window.history.replaceState(cur_hash, '', '#' + cur_hash); window.history.pushState(detail_str, '', '#' + detail_str); } catch(ex) { } } this.navigate_to_dest(detail_str, this.get_containing_page(target)); e.preventDefault(); }, /** * @param{string} detail_str may come from user provided hashtag, need sanitizing * @param{Page=} src_page page containing the source event (e.g. link) */ navigate_to_dest : function(detail_str, src_page) { try { var detail = JSON.parse(detail_str); } catch(e) { return; } if(!(detail instanceof Array)) return; var target_page_no = detail[0]; var page_map = this.page_map; if (!(target_page_no in page_map)) return; var target_page_idx = page_map[target_page_no]; var target_page = this.pages[target_page_idx]; for (var i = 2, l = detail.length; i < l; ++i) { var d = detail[i]; if(!((d === null) || (typeof d === 'number'))) return; } while(detail.length < 6) detail.push(null); // cur_page might be undefined, e.g. from Outline var cur_page = src_page || this.pages[this.cur_page_idx]; var cur_pos = cur_page.view_position(); cur_pos = transform(cur_page.ictm, [cur_pos[0], cur_page.height()-cur_pos[1]]); var zoom = this.scale; var pos = [0,0]; var upside_down = true; var ok = false; // position specified in `detail` are in the raw coordinate system of the page (unscaled) var scale = this.scale; // TODO: fitb* // TODO: BBox switch(detail[1]) { case 'XYZ': pos = [ (detail[2] === null) ? cur_pos[0] : detail[2] * scale , (detail[3] === null) ? cur_pos[1] : detail[3] * scale ]; zoom = detail[4]; if ((zoom === null) || (zoom === 0)) zoom = this.scale; ok = true; break; case 'Fit': case 'FitB': pos = [0,0]; ok = true; break; case 'FitH': case 'FitBH': pos = [0, (detail[2] === null) ? cur_pos[1] : detail[2] * scale]; ok = true; break; case 'FitV': case 'FitBV': pos = [(detail[2] === null) ? cur_pos[0] : detail[2] * scale, 0]; ok = true; break; case 'FitR': /* locate the top-left corner of the rectangle */ // TODO pos = [detail[2] * scale, detail[5] * scale]; upside_down = false; ok = true; break; default: break; } if (!ok) return; this.rescale(zoom, false); var self = this; /** * page should have type Page * @param{Page} page */ var transform_and_scroll = function(page) { pos = transform(page.ctm, pos); if (upside_down) { pos[1] = page.height() - pos[1]; } self.scroll_to(target_page_idx, pos); }; if (target_page.loaded) { transform_and_scroll(target_page); } else { // TODO: scroll_to may finish before load_page // Scroll to the exact position once loaded. this.load_page(target_page_idx, undefined, transform_and_scroll); // In the meantime page gets loaded, scroll approximately position for maximum responsiveness. this.scroll_to(target_page_idx); } }, /** * @param{number} page_idx * @param{Array.=} pos [x,y] where (0,0) is the top-left corner */ scroll_to : function(page_idx, pos) { var pl = this.pages; if ((page_idx < 0) || (page_idx >= pl.length)) return; var target_page = pl[page_idx]; var cur_target_pos = target_page.view_position(); if (pos === undefined) pos = [0,0]; var container = this.container; container.scrollLeft += pos[0] - cur_target_pos[0]; container.scrollTop += pos[1] - cur_target_pos[1]; }, /** * generate the hash for the current view */ get_current_view_hash : function() { var detail = []; var cur_page = this.pages[this.cur_page_idx]; detail.push(cur_page.num); detail.push('XYZ'); var cur_pos = cur_page.view_position(); cur_pos = transform(cur_page.ictm, [cur_pos[0], cur_page.height()-cur_pos[1]]); detail.push(cur_pos[0] / this.scale); detail.push(cur_pos[1] / this.scale); detail.push(this.scale); return JSON.stringify(detail); } }; // export pdf2htmlEX.Viewer pdf2htmlEX['Viewer'] = Viewer;