/* * kthoom.js * * Licensed under the MIT License * * Copyright(c) 2011 Google Inc. * Copyright(c) 2011 antimatter15 */ /* Reference Documentation: * Web Workers: http://www.whatwg.org/specs/web-workers/current-work/ * Web Workers in Mozilla: https://developer.mozilla.org/En/Using_web_workers * File API (FileReader): http://www.w3.org/TR/FileAPI/ * Typed Arrays: http://www.khronos.org/registry/typedarray/specs/latest/#6 */ /* global bitjs */ if (window.opera) { window.console.log = function(str) { opera.postError(str); }; } var kthoom; // gets the element with the given id function getElem(id) { if (document.documentElement.querySelector) { // querySelector lookup return document.body.querySelector("#" + id); } // getElementById lookup return document.getElementById(id); } if (window.kthoom === undefined) { kthoom = {}; } // key codes kthoom.Key = { ESCAPE: 27, LEFT: 37, UP: 38, RIGHT: 39, DOWN: 40, A: 65, B: 66, C: 67, D: 68, E: 69, F: 70, G: 71, H: 72, I: 73, J: 74, K: 75, L: 76, M: 77, N: 78, O: 79, P: 80, Q: 81, R: 82, S: 83, T: 84, U: 85, V: 86, W: 87, X: 88, Y: 89, Z: 90, QUESTION_MARK: 191, LEFT_SQUARE_BRACKET: 219, RIGHT_SQUARE_BRACKET: 221 }; // The rotation orientation of the comic. kthoom.rotateTimes = 0; // global variables var unarchiver = null; var currentImage = 0; var imageFiles = []; var imageFilenames = []; var totalImages = 0; var lastCompletion = 0; var hflip = false, vflip = false, fitMode = kthoom.Key.B; var canKeyNext = true, canKeyPrev = true; kthoom.saveSettings = function() { localStorage.kthoomSettings = JSON.stringify({ rotateTimes: kthoom.rotateTimes, hflip: hflip, vflip: vflip, fitMode: fitMode }); }; kthoom.loadSettings = function() { try { if (localStorage.kthoomSettings.length < 10){ return; } var s = JSON.parse(localStorage.kthoomSettings); kthoom.rotateTimes = s.rotateTimes; hflip = s.hflip; vflip = s.vflip; fitMode = s.fitMode; } catch (err) { alert("Error load settings"); } }; var createURLFromArray = function(array, mimeType) { var offset = array.byteOffset, len = array.byteLength; var url; var blob; // TODO: Move all this browser support testing to a common place // and do it just once. // Blob constructor, see http://dev.w3.org/2006/webapi/FileAPI/#dfn-Blob. if (typeof Blob === "function") { blob = new Blob([array], {type: mimeType}); } else { throw "Browser support for Blobs is missing."; } if (blob.slice) { blob = blob.slice(offset, offset + len, mimeType); } else { throw "Browser support for Blobs is missing."; } if ((typeof URL !== "function" && typeof URL !== "object") || typeof URL.createObjectURL !== "function") { throw "Browser support for Object URLs is missing"; } return URL.createObjectURL(blob); }; // Stores an image filename and its data: URI. // TODO: investigate if we really need to store as base64 (leave off ;base64 and just // non-safe URL characters are encoded as %xx ?) // This would save 25% on memory since base64-encoded strings are 4/3 the size of the binary kthoom.ImageFile = function(file) { this.filename = file.filename; var fileExtension = file.filename.split(".").pop().toLowerCase(); var mimeType = fileExtension === "png" ? "image/png" : (fileExtension === "jpg" || fileExtension === "jpeg") ? "image/jpeg" : fileExtension === "gif" ? "image/gif" : null; this.dataURI = createURLFromArray(file.fileData, mimeType); this.data = file; }; kthoom.initProgressMeter = function() { var svgns = "http://www.w3.org/2000/svg"; var pdiv = $("#progress")[0]; var svg = document.createElementNS(svgns, "svg"); svg.style.width = "100%"; svg.style.height = "100%"; var defs = document.createElementNS(svgns, "defs"); var patt = document.createElementNS(svgns, "pattern"); patt.id = "progress_pattern"; patt.setAttribute("width", "30"); patt.setAttribute("height", "20"); patt.setAttribute("patternUnits", "userSpaceOnUse"); var rect = document.createElementNS(svgns, "rect"); rect.setAttribute("width", "100%"); rect.setAttribute("height", "100%"); rect.setAttribute("fill", "#cc2929"); var poly = document.createElementNS(svgns, "polygon"); poly.setAttribute("fill", "yellow"); poly.setAttribute("points", "15,0 30,0 15,20 0,20"); patt.appendChild(rect); patt.appendChild(poly); defs.appendChild(patt); svg.appendChild(defs); var g = document.createElementNS(svgns, "g"); var outline = document.createElementNS(svgns, "rect"); outline.setAttribute("y", "1"); outline.setAttribute("width", "100%"); outline.setAttribute("height", "15"); outline.setAttribute("fill", "#777"); outline.setAttribute("stroke", "white"); outline.setAttribute("rx", "5"); outline.setAttribute("ry", "5"); g.appendChild(outline); var title = document.createElementNS(svgns, "text"); title.id = "progress_title"; title.appendChild(document.createTextNode("0%")); title.setAttribute("y", "13"); title.setAttribute("x", "99.5%"); title.setAttribute("fill", "white"); title.setAttribute("font-size", "12px"); title.setAttribute("text-anchor", "end"); g.appendChild(title); var meter = document.createElementNS(svgns, "rect"); meter.id = "meter"; meter.setAttribute("width", "0%"); meter.setAttribute("height", "17"); meter.setAttribute("fill", "url(#progress_pattern)"); meter.setAttribute("rx", "5"); meter.setAttribute("ry", "5"); var meter2 = document.createElementNS(svgns, "rect"); meter2.id = "meter2"; meter2.setAttribute("width", "0%"); meter2.setAttribute("height", "17"); meter2.setAttribute("opacity", "0.8"); meter2.setAttribute("fill", "#007fff"); meter2.setAttribute("rx", "5"); meter2.setAttribute("ry", "5"); g.appendChild(meter); g.appendChild(meter2); var page = document.createElementNS(svgns, "text"); page.id = "page"; page.appendChild(document.createTextNode("0/0")); page.setAttribute("y", "13"); page.setAttribute("x", "0.5%"); page.setAttribute("fill", "white"); page.setAttribute("font-size", "12px"); g.appendChild(page); svg.appendChild(g); pdiv.appendChild(svg); var l; svg.onclick = function(e) { for (var x = pdiv, l = 0; x !== document.documentElement; x = x.parentNode) l += x.offsetLeft; var page = Math.max(1, Math.ceil(((e.clientX - l) / pdiv.offsetWidth) * totalImages)) - 1; currentImage = page; updatePage(); }; } kthoom.setProgressMeter = function(pct, optLabel) { pct = (pct * 100); var part = 1 / totalImages; var remain = ((pct - lastCompletion) / 100) / part; var fract = Math.min(1, remain); var smartpct = ((imageFiles.length / totalImages) + (fract * part)) * 100; if (totalImages === 0) smartpct = pct; // + Math.min((pct - lastCompletion), 100/totalImages * 0.9 + (pct - lastCompletion - 100/totalImages)/2, 100/totalImages); var oldval = parseFloat(getElem("meter").getAttribute("width")); if (isNaN(oldval)) oldval = 0; var weight = 0.5; smartpct = ((weight * smartpct) + ((1 - weight) * oldval)); if (pct === 100) smartpct = 100; if (!isNaN(smartpct)) { getElem("meter").setAttribute("width", smartpct + "%"); } var title = getElem("progress_title"); while (title.firstChild) title.removeChild(title.firstChild); var labelText = pct.toFixed(2) + "% " + imageFiles.length + "/" + totalImages + ""; if (optLabel) { labelText = optLabel + " " + labelText; } title.appendChild(document.createTextNode(labelText)); getElem("meter2").setAttribute("width", 100 * (totalImages === 0 ? 0 : ((currentImage + 1) / totalImages)) + "%"); var titlePage = getElem("page"); while (titlePage.firstChild) titlePage.removeChild(titlePage.firstChild); titlePage.appendChild(document.createTextNode( (currentImage + 1) + "/" + totalImages )); if (pct > 0) { //getElem('nav').className = ''; getElem("progress").className = ""; } } function loadFromArrayBuffer(ab) { var start = (new Date).getTime(); var h = new Uint8Array(ab, 0, 10); var pathToBitJS = "../../static/js/"; if (h[0] === 0x52 && h[1] === 0x61 && h[2] === 0x72 && h[3] === 0x21) { //Rar! unarchiver = new bitjs.archive.Unrarrer(ab, pathToBitJS); } else if (h[0] === 80 && h[1] === 75) { //PK (Zip) unarchiver = new bitjs.archive.Unzipper(ab, pathToBitJS); } else { // Try with tar unarchiver = new bitjs.archive.Untarrer(ab, pathToBitJS); } // Listen for UnarchiveEvents. if (unarchiver) { unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.PROGRESS, function(e) { var percentage = e.currentBytesUnarchived / e.totalUncompressedBytesInArchive; totalImages = e.totalFilesInArchive; kthoom.setProgressMeter(percentage, "Unzipping"); // display nav lastCompletion = percentage * 100; }); unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.EXTRACT, function(e) { // convert DecompressedFile into a bunch of ImageFiles if (e.unarchivedFile) { var f = e.unarchivedFile; // add any new pages based on the filename if (imageFilenames.indexOf(f.filename) === -1) { imageFilenames.push(f.filename); imageFiles.push(new kthoom.ImageFile(f)); } } // display first page if we haven't yet if (imageFiles.length === currentImage + 1) { updatePage(); } }); unarchiver.addEventListener(bitjs.archive.UnarchiveEvent.Type.FINISH, function() { var diff = ((new Date).getTime() - start) / 1000; console.log("Unarchiving done in " + diff + "s"); }); unarchiver.start(); } else { alert("Some error"); } } function updatePage() { var title = getElem("page"); while (title.firstChild) title.removeChild(title.firstChild); title.appendChild(document.createTextNode( (currentImage + 1 ) + "/" + totalImages )); getElem("meter2").setAttribute("width", 100 * (totalImages === 0 ? 0 : ((currentImage + 1 ) / totalImages)) + "%"); if (imageFiles[currentImage]) { setImage(imageFiles[currentImage].dataURI); } else { setImage("loading"); } } function setImage(url) { var canvas = $("#mainImage")[0]; var x = $("#mainImage")[0].getContext("2d"); $("#mainText").hide(); if (url === "loading") { updateScale(true); canvas.width = innerWidth - 100; canvas.height = 200; x.fillStyle = "red"; x.font = "50px sans-serif"; x.strokeStyle = "black"; x.fillText("Loading Page #" + (currentImage + 1), 100, 100); } else { if ($("body").css("scrollHeight") / innerHeight > 1) { $("body").css("overflowY", "scroll"); } var img = new Image(); img.onerror = function() { canvas.width = innerWidth - 100; canvas.height = 300; updateScale(true); x.fillStyle = "orange"; x.font = "50px sans-serif"; x.strokeStyle = "black"; x.fillText("Page #" + (currentImage + 1) + " (" + imageFiles[currentImage].filename + ")", 100, 100); x.fillStyle = "red"; x.fillText("Is corrupt or not an image", 100, 200); var xhr = new XMLHttpRequest(); if (/(html|htm)$/.test(imageFiles[currentImage].filename)) { xhr.open("GET", url, true); xhr.onload = function() { //document.getElementById('mainText').style.display = ''; $("#mainText").css("display", ""); $("#mainText").innerHTML(""); } xhr.send(null); } else if (!/(jpg|jpeg|png|gif)$/.test(imageFiles[currentImage].filename) && imageFiles[currentImage].data.uncompressedSize < 10 * 1024) { xhr.open("GET", url, true); xhr.onload = function() { $("#mainText").css("display", ""); $("#mainText").innerText(xhr.responseText); }; xhr.send(null); } }; img.onload = function() { var h = img.height, w = img.width, sw = w, sh = h; kthoom.rotateTimes = (4 + kthoom.rotateTimes) % 4; x.save(); if (kthoom.rotateTimes % 2 === 1) { sh = w; sw = h; } canvas.height = sh; canvas.width = sw; x.translate(sw / 2, sh / 2); x.rotate(Math.PI / 2 * kthoom.rotateTimes); x.translate(-w / 2, -h / 2); if (vflip) { x.scale(1, -1); x.translate(0, -h); } if (hflip) { x.scale(-1, 1); x.translate(-w, 0); } canvas.style.display = "none"; scrollTo(0, 0); x.drawImage(img, 0, 0); updateScale(); canvas.style.display = ""; $("body").css("overflowY", ""); x.restore(); }; img.src = url; } } function showPrevPage() { currentImage--; if (currentImage < 0) { // Freeze on the current page. currentImage++; } else { updatePage(); } } function showNextPage() { currentImage++; if (currentImage >= Math.max(totalImages, imageFiles.length)) { // Freeze on the current page. currentImage--; } else { updatePage(); } } function updateScale(clear) { var mainImageStyle = getElem("mainImage").style; mainImageStyle.width = ""; mainImageStyle.height = ""; mainImageStyle.maxWidth = ""; mainImageStyle.maxHeight = ""; var maxheight = innerHeight - 15; if (!/main/.test(getElem("titlebar").className)) { maxheight -= 25; } if (clear || fitMode === kthoom.Key.N) { } else if (fitMode === kthoom.Key.B) { mainImageStyle.maxWidth = "100%"; mainImageStyle.maxHeight = maxheight + "px"; } else if (fitMode === kthoom.Key.H) { mainImageStyle.height = maxheight + "px"; } else if (fitMode === kthoom.Key.W) { mainImageStyle.width = "100%"; } kthoom.saveSettings(); } function keyHandler(evt) { var code = evt.keyCode; if ($("#progress").css("display") === "none"){ return; } canKeyNext = (($("body").css("offsetWidth") + $("body").css("scrollLeft")) / $("body").css("scrollWidth")) >= 1; canKeyPrev = (scrollX <= 0); if (evt.ctrlKey || evt.shiftKey || evt.metaKey) return; switch (code) { case kthoom.Key.LEFT: if (canKeyPrev) showPrevPage(); break; case kthoom.Key.RIGHT: if (canKeyNext) showNextPage(); break; case kthoom.Key.L: kthoom.rotateTimes--; if (kthoom.rotateTimes < 0) { kthoom.rotateTimes = 3; } updatePage(); break; case kthoom.Key.R: kthoom.rotateTimes++; if (kthoom.rotateTimes > 3) { kthoom.rotateTimes = 0; } updatePage(); break; case kthoom.Key.F: if (!hflip && !vflip) { hflip = true; } else if (hflip === true) { vflip = true; hflip = false; } else if (vflip === true) { vflip = false; } updatePage(); break; case kthoom.Key.W: fitMode = kthoom.Key.W; updateScale(); break; case kthoom.Key.H: fitMode = kthoom.Key.H; updateScale(); break; case kthoom.Key.B: fitMode = kthoom.Key.B; updateScale(); break; case kthoom.Key.N: fitMode = kthoom.Key.N; updateScale(); break; default: //console.log('KeyCode = ' + code); break; } } function init(filename) { if (!window.FileReader) { alert("Sorry, kthoom will not work with your browser because it does not support the File API. Please try kthoom with Chrome 12+ or Firefox 7+"); } else { var request = new XMLHttpRequest(); request.open("GET", filename); request.responseType = "arraybuffer"; request.setRequestHeader("X-Test", "test1"); request.setRequestHeader("X-Test", "test2"); request.addEventListener("load", function(event) { if (request.status >= 200 && request.status < 300) { loadFromArrayBuffer(request.response); } else { console.warn(request.statusText, request.responseText); } }); request.send(); kthoom.initProgressMeter(); document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : ""; updateScale(true); kthoom.loadSettings(); $(document).keydown(keyHandler); $(window).resize(function() { var f = (screen.width - innerWidth < 4 && screen.height - innerHeight < 4); getElem("titlebar").className = f ? "main" : ""; updateScale(); }); $("#mainImage").click(function(evt) { // Firefox does not support offsetX/Y so we have to manually calculate // where the user clicked in the image. var mainContentWidth = $("#mainContent").width(); var mainContentHeight = $("#mainContent").height(); var comicWidth = evt.target.clientWidth; var comicHeight = evt.target.clientHeight; var offsetX = (mainContentWidth - comicWidth) / 2; var offsetY = (mainContentHeight - comicHeight) / 2; var clickX = !!evt.offsetX ? evt.offsetX : (evt.clientX - offsetX); var clickY = !!evt.offsetY ? evt.offsetY : (evt.clientY - offsetY); // Determine if the user clicked/tapped the left side or the // right side of the page. var clickedPrev = false; switch (kthoom.rotateTimes) { case 0: clickedPrev = clickX < (comicWidth / 2); break; case 1: clickedPrev = clickY < (comicHeight / 2); break; case 2: clickedPrev = clickX > (comicWidth / 2); break; case 3: clickedPrev = clickY > (comicHeight / 2); break; } if (clickedPrev) { showPrevPage(); } else { showNextPage(); } }); } }