/* vim: set shiftwidth=2 tabstop=2 autoindent cindent expandtab filetype=javascript : */ /* * pdf2htmlEX.js : a simple demo UI for pdf2htmlEX * * handles UI events/actions/effects * Copyright 2012,2013 Lu Wang */ 'use strict'; var pdf2htmlEX = (function(){ var CSS_CLASS_NAMES = { page_frame : '@CSS_PAGE_FRAME_CN@', page_decoration : '@CSS_PAGE_DECORATION_CN@', page_content_box : '@CSS_PAGE_CONTENT_BOX_CN@', page_data : '@CSS_PAGE_DATA_CN@', background_image : '@CSS_BACKGROUND_IMAGE_CN@', link : '@CSS_LINK_CN@', __dummy__ : 'no comma' }; var DEFAULT_PAGES_TO_PRELOAD = 3; var pdf2htmlEX = new Object(); var EPS = 1e-6; var invert = function(ctm) { var det = ctm[0] * ctm[3] - ctm[1] * ctm[2]; var ictm = new Array(); ictm[0] = ctm[3] / det; ictm[1] = -ctm[1] / det; ictm[2] = -ctm[2] / det; ictm[3] = ctm[0] / det; ictm[4] = (ctm[2] * ctm[5] - ctm[3] * ctm[4]) / det; ictm[5] = (ctm[1] * ctm[4] - ctm[0] * ctm[5]) / det; return ictm; }; var transform = function(ctm, pos) { return [ctm[0] * pos[0] + ctm[2] * pos[1] + ctm[4] ,ctm[1] * pos[0] + ctm[3] * pos[1] + ctm[5]]; }; var Page = function(page, container) { if(page == undefined) return; this.loaded = false; this.$p = $(page); this.$container = $(container); this.n = parseInt(this.$p.data('page-no'), 16); this.$b = $('.'+CSS_CLASS_NAMES['page_content_box'], this.$p); this.$d = this.$p.parent('.'+CSS_CLASS_NAMES['page_decoration']); this.h = this.$p.height(); // Need to make rescale work when page_content_box is not loaded, yet this.w = this.$p.width(); // if page is loaded if (this.$b.length > 0) { /* * scale ratios * * default_r : the first one * set_r : last set * cur_r : currently using */ this.default_r = this.set_r = this.cur_r = this.$p.height() / this.$b.height(); this.data = $($('.'+CSS_CLASS_NAMES['page_data'], this.$p)[0]).data('data'); this.ctm = this.data.ctm; this.ictm = invert(this.ctm); this.loaded = true; } }; $.extend(Page.prototype, { /* hide & show are for contents, the page frame is still there */ hide : function(){ this.$b.removeClass('opened'); }, show : function(){ if(Math.abs(this.set_r - this.cur_r) > EPS) { this.cur_r = this.set_r; this.$b.css('transform', 'scale('+this.cur_r.toFixed(3)+')'); } this.$b.addClass('opened'); }, rescale : function(ratio, is_relative) { if(ratio == 0) { this.set_r = this.default_r; } else if (is_relative) { this.set_r *= ratio; } else { this.set_r = ratio; } /* wait for redraw */ this.hide(); this.$p.height(this.$b.height() * this.set_r); this.$p.width(this.$b.width() * this.set_r); }, /* return if any part of this page is shown in the container */ is_visible : function() { var off = this.position(); return !((off[1] > this.height()) || (off[1] + this.$container.height() < 0)); }, /* return if this page or any neighbor of it is visible */ is_nearly_visible : function() { var off = this.position(); /* I should use the height of the previous page or the next page here * but since they are not easily available, just use '*2', which should be a good estimate in most cases */ return !((off[1] > this.height() * 2) || (off[1] + this.$container.height() * 2 < 0)); }, /* return the coordinate of the top-left corner of container * in our coordinate system */ position : function () { var off = this.$p.offset(); var off_c = this.$container.offset(); return [off_c.left-off.left, off_c.top-off.top]; }, height : function() { return this.$p.height(); } }); pdf2htmlEX.Viewer = function(config) { this.container_id = config['container_id']; this.sidebar_id = config['sidebar_id']; this.outline_id = config['outline_id']; this.pages_to_preload = config['pages_to_preload'] || DEFAULT_PAGES_TO_PRELOAD; this.pages_loading = {}; this.init_before_loading_content(); var _ = this; $(function(){_.init_after_loading_content();}); }; $.extend(pdf2htmlEX.Viewer.prototype, { /* Constants */ render_timeout : 100, scale_step : 0.9, init_before_loading_content : function() { /*hide all pages before loading, will reveal only visible ones later */ this.pre_hide_pages(); }, init_after_loading_content : function() { this.$sidebar = $('#'+this.sidebar_id); this.$outline = $('#'+this.outline_id); this.$container = $('#'+this.container_id); // Open the outline if nonempty if(this.$outline.children().length > 0) { this.$sidebar.addClass('opened'); } this.find_pages(); // register schedule rendering var _ = this; this.$container.scroll(function(){ _.schedule_render(); }); //this.zoom_fixer(); // handle links this.$container.add(this.$outline).on('click', '.'+CSS_CLASS_NAMES['link'], this, this.link_handler); this.render(); }, find_pages : function() { var new_pages = new Array(); var $pl= $('.'+CSS_CLASS_NAMES['page_frame'], this.$container); /* don't use for(..in..) */ for(var i = 0, l = $pl.length; i < l; ++i) { var p = new Page($pl[i], this.$container); new_pages[p.n] = p; } this.pages = new_pages; }, load_page : function(idx, pages_to_preload, successCallback, errorCallback) { if (idx >= this.pages.length) return; // Page does not exist if (this.pages[idx].loaded) return; // Page is loaded if (this.pages_loading[idx]) return; // Page is already loading var page_no_hex = idx.toString(16); var $pf = this.$container.find('#' + CSS_CLASS_NAMES['page_frame'] + page_no_hex); if($pf.length == 0) return; // Page does not exist var _ = this; var url = $pf.data('page-url'); if (url && url.length > 0) { this.pages_loading[idx] = true; // Set semaphore $.ajax({ url: url, dataType: 'text' }).done(function(data){ _.pages[idx].$p.parent().replaceWith(data); var $new_pf = _.$container.find('#' + CSS_CLASS_NAMES['page_frame'] + page_no_hex); _.pages[idx] = new Page($new_pf, _.$container); _.pages[idx].rescale(_.scale); _.schedule_render(); // disable background image dragging $new_pf.find('.'+CSS_CLASS_NAMES['background_image']).on('dragstart', function(e){return false;}); // Reset loading token delete _.pages_loading[idx]; if (successCallback) successCallback(); } ).fail(function(jqXHR, textStatus, errorThrown){ console.error('error loading page ' + idx + ': ' + textStatus); // Reset loading token delete _.pages_loading[idx]; if (errorCallback) errorCallback(); }); } // Concurrent prefetch of the next pages if (pages_to_preload === undefined) pages_to_preload = this.pages_to_preload; if (--pages_to_preload > 0) _.load_page(idx+1, pages_to_preload); }, 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'); n.type = 'text/css'; if (n.styleSheet) { n.styleSheet.cssText = s; } else { n.appendChild(document.createTextNode(s)); } document.getElementsByTagName('head')[0].appendChild(n); }, hide_pages : function() { var pl = this.pages; for(var i in pl) pl[i].hide(); }, render : function () { /* hide (positional) invisible pages */ var pl = this.pages; for(var i in pl) { var p = pl[i]; if(p.is_nearly_visible()){ if (p.loaded) { p.show(); } else this.load_page(p.n); } else { p.hide(); } } }, schedule_render : function() { if(this.render_timer) clearTimeout(this.render_timer); var _ = this; this.render_timer = setTimeout(function () { _.render(); }, this.render_timeout); }, zoom_fixer : 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 _ = this; // Firefox specific event, so that we can prevent browser from zooming $(window).on('DOMMouseScroll', function(e) { if (e.ctrlKey) { e.preventDefault(); _.rescale(Math.pow(_.scale_step, e.detail), true); } }); $(window).on('keydown', function keydown(e) { if (e.ctrlKey || e.metaKey) { switch (e.keyCode) { case 61: // FF/Mac '=' case 107: // FF '+' and '=' case 187: // Chrome '+' _.rescale(1.0 / _.scale_step, true); break; case 173: // FF/Mac '-' case 109: // FF '-' case 189: // Chrome '-' _.rescale(_.scale_step, true); break; case 48: // '0' _.rescale(0, false); break; default: return; } e.preventDefault(); } }); }, rescale : function (ratio, is_relative) { var pl = this.pages; for(var i in pl) { pl[i].rescale(ratio, is_relative); } this.schedule_render(); }, get_containing_page : function(obj) { /* get the page obj containing obj */ var p = obj.closest('.'+CSS_CLASS_NAMES['page_frame'])[0]; return p && this.pages[(new Page(p)).n]; }, link_handler : function (e) { var _ = e.data; var t = $(e.currentTarget); var cur_pos = [0,0]; // cur_page might be undefined, e.g. from Outline var cur_page = _.get_containing_page(t); if(cur_page != undefined) { cur_pos = cur_page.position(); //get the coordinates in default user system cur_pos = transform(cur_page.ictm, [cur_pos[0], cur_page.height()-cur_pos[1]]); } var detail_str = t.attr('data-dest-detail'); if(detail_str == undefined) return; var ok = false; var detail = JSON.parse(detail_str); var target_page = _.pages[detail[0]]; if(target_page == undefined) return; var pos = [0,0]; var upside_down = true; // TODO: zoom // TODO: BBox switch(detail[1]) { case 'XYZ': pos = [(detail[2] == null) ? cur_pos[0] : detail[2] ,(detail[3] == null) ? cur_pos[1] : detail[3]]; 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]] ok = true; break; case 'FitV': case 'FitBV': pos = [(detail[2] == null) ? cur_pos[0] : detail[2], 0]; ok = true; break; case 'FitR': /* locate the top-left corner of the rectangle */ pos = [detail[2], detail[5]]; upside_down = false; ok = true; break; default: ok = false; break; } if(ok) { var transform_and_scroll = function() { pos = transform(target_page.ctm, pos); if(upside_down) { pos[1] = target_page.height() - pos[1]; } _.scroll_to(detail[0], pos); } if (target_page.loaded) { transform_and_scroll(); } else { // Scroll to the exact position once loaded. _.load_page(target_page.n, 1, function() { target_page = _.pages[target_page.n]; // Refresh reference transform_and_scroll(); }); // In the meantime page gets loaded, scroll approximately position for maximum responsiveness. _.scroll_to(detail[0], [0,0]); } e.preventDefault(); } }, /* pos=[x,y], where (0,0) is the top-left corner */ scroll_to : function(pageno, pos) { var target_page = this.pages[pageno]; if(target_page == undefined) return; var cur_target_pos = target_page.position(); this.$container.scrollLeft(this.$container.scrollLeft()-cur_target_pos[0]+pos[0]); this.$container.scrollTop(this.$container.scrollTop()-cur_target_pos[1]+pos[1]); }, __last_member__ : 'no comma' /*,*/ }); return pdf2htmlEX; })();