Merge branch 'master' into Develop

# Conflicts:
#	test/Calibre-Web TestSummary_Linux.html
This commit is contained in:
Ozzie Isaacs 2023-02-27 18:54:02 +01:00
commit 5c5db34a52
7 changed files with 386 additions and 212 deletions

View File

@ -21,6 +21,7 @@ import zipfile
from lxml import etree from lxml import etree
from . import isoLanguages, cover from . import isoLanguages, cover
from . import config
from .helper import split_authors from .helper import split_authors
from .constants import BookMeta from .constants import BookMeta
@ -28,18 +29,40 @@ from .constants import BookMeta
def _extract_cover(zip_file, cover_file, cover_path, tmp_file_name): def _extract_cover(zip_file, cover_file, cover_path, tmp_file_name):
if cover_file is None: if cover_file is None:
return None return None
else:
cf = extension = None
zip_cover_path = os.path.join(cover_path, cover_file).replace('\\', '/')
prefix = os.path.splitext(tmp_file_name)[0] cf = extension = None
tmp_cover_name = prefix + '.' + os.path.basename(zip_cover_path) zip_cover_path = os.path.join(cover_path, cover_file).replace('\\', '/')
ext = os.path.splitext(tmp_cover_name)
if len(ext) > 1: prefix = os.path.splitext(tmp_file_name)[0]
extension = ext[1].lower() tmp_cover_name = prefix + '.' + os.path.basename(zip_cover_path)
if extension in cover.COVER_EXTENSIONS: ext = os.path.splitext(tmp_cover_name)
cf = zip_file.read(zip_cover_path) if len(ext) > 1:
return cover.cover_processing(tmp_file_name, cf, extension) extension = ext[1].lower()
if extension in cover.COVER_EXTENSIONS:
cf = zip_file.read(zip_cover_path)
return cover.cover_processing(tmp_file_name, cf, extension)
def get_epub_layout(book, book_data):
ns = {
'n': 'urn:oasis:names:tc:opendocument:xmlns:container',
'pkg': 'http://www.idpf.org/2007/opf',
}
file_path = os.path.normpath(os.path.join(config.config_calibre_dir, book.path, book_data.name + "." + book_data.format.lower()))
epubZip = zipfile.ZipFile(file_path)
txt = epubZip.read('META-INF/container.xml')
tree = etree.fromstring(txt)
cfname = tree.xpath('n:rootfiles/n:rootfile/@full-path', namespaces=ns)[0]
cf = epubZip.read(cfname)
tree = etree.fromstring(cf)
p = tree.xpath('/pkg:package/pkg:metadata', namespaces=ns)[0]
layout = p.xpath('pkg:meta[@property="rendition:layout"]/text()', namespaces=ns)
if len(layout) == 0:
return None
else:
return layout[0]
def get_epub_info(tmp_file_path, original_file_name, original_file_extension): def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
@ -134,40 +157,40 @@ def get_epub_info(tmp_file_path, original_file_name, original_file_extension):
def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path): def parse_epub_cover(ns, tree, epub_zip, cover_path, tmp_file_path):
cover_section = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns) cover_section = tree.xpath("/pkg:package/pkg:manifest/pkg:item[@id='cover-image']/@href", namespaces=ns)
cover_file = None
# if len(cover_section) > 0:
for cs in cover_section: for cs in cover_section:
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path) cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
if cover_file: if cover_file:
break return cover_file
if not cover_file:
meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns) meta_cover = tree.xpath("/pkg:package/pkg:metadata/pkg:meta[@name='cover']/@content", namespaces=ns)
if len(meta_cover) > 0: if len(meta_cover) > 0:
cover_section = tree.xpath(
"/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns)
if not cover_section:
cover_section = tree.xpath( cover_section = tree.xpath(
"/pkg:package/pkg:manifest/pkg:item[@id='"+meta_cover[0]+"']/@href", namespaces=ns) "/pkg:package/pkg:manifest/pkg:item[@properties='" + meta_cover[0] + "']/@href", namespaces=ns)
if not cover_section: else:
cover_section = tree.xpath( cover_section = tree.xpath("/pkg:package/pkg:guide/pkg:reference/@href", namespaces=ns)
"/pkg:package/pkg:manifest/pkg:item[@properties='" + meta_cover[0] + "']/@href", namespaces=ns)
cover_file = None
for cs in cover_section:
if cs.endswith('.xhtml') or cs.endswith('.html'):
markup = epub_zip.read(os.path.join(cover_path, cs))
markup_tree = etree.fromstring(markup)
# no matter xhtml or html with no namespace
img_src = markup_tree.xpath("//*[local-name() = 'img']/@src")
# Alternative image source
if not len(img_src):
img_src = markup_tree.xpath("//attribute::*[contains(local-name(), 'href')]")
if len(img_src):
# img_src maybe start with "../"" so fullpath join then relpath to cwd
filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(cover_path, cover_section[0])),
img_src[0]))
cover_file = _extract_cover(epub_zip, filename, "", tmp_file_path)
else: else:
cover_section = tree.xpath("/pkg:package/pkg:guide/pkg:reference/@href", namespaces=ns) cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
for cs in cover_section: if cover_file:
filetype = cs.rsplit('.', 1)[-1] break
if filetype == "xhtml" or filetype == "html": # if cover is (x)html format
markup = epub_zip.read(os.path.join(cover_path, cs))
markup_tree = etree.fromstring(markup)
# no matter xhtml or html with no namespace
img_src = markup_tree.xpath("//*[local-name() = 'img']/@src")
# Alternative image source
if not len(img_src):
img_src = markup_tree.xpath("//attribute::*[contains(local-name(), 'href')]")
if len(img_src):
# img_src maybe start with "../"" so fullpath join then relpath to cwd
filename = os.path.relpath(os.path.join(os.path.dirname(os.path.join(cover_path, cover_section[0])),
img_src[0]))
cover_file = _extract_cover(epub_zip, filename, "", tmp_file_path)
else:
cover_file = _extract_cover(epub_zip, cs, cover_path, tmp_file_path)
if cover_file: break
return cover_file return cover_file

View File

@ -21,6 +21,7 @@ import base64
import datetime import datetime
import os import os
import uuid import uuid
import zipfile
from time import gmtime, strftime from time import gmtime, strftime
import json import json
from urllib.parse import unquote from urllib.parse import unquote
@ -46,6 +47,7 @@ import requests
from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status from . import config, logger, kobo_auth, db, calibre_db, helper, shelf as shelf_lib, ub, csrf, kobo_sync_status
from . import isoLanguages from . import isoLanguages
from .epub import get_epub_layout
from .constants import sqlalchemy_version2, COVER_THUMBNAIL_SMALL from .constants import sqlalchemy_version2, COVER_THUMBNAIL_SMALL
from .helper import get_download_link from .helper import get_download_link
from .services import SyncToken as SyncToken from .services import SyncToken as SyncToken
@ -459,16 +461,21 @@ def get_metadata(book):
continue continue
for kobo_format in KOBO_FORMATS[book_data.format]: for kobo_format in KOBO_FORMATS[book_data.format]:
# log.debug('Id: %s, Format: %s' % (book.id, kobo_format)) # log.debug('Id: %s, Format: %s' % (book.id, kobo_format))
download_urls.append( try:
{ if get_epub_layout(book, book_data) == 'pre-paginated':
"Format": kobo_format, kobo_format = 'EPUB3FL'
"Size": book_data.uncompressed_size, download_urls.append(
"Url": get_download_url_for_book(book, book_data.format), {
# The Kobo forma accepts platforms: (Generic, Android) "Format": kobo_format,
"Platform": "Generic", "Size": book_data.uncompressed_size,
# "DrmType": "None", # Not required "Url": get_download_url_for_book(book, book_data.format),
} # The Kobo forma accepts platforms: (Generic, Android)
) "Platform": "Generic",
# "DrmType": "None", # Not required
}
)
except (zipfile.BadZipfile, FileNotFoundError) as e:
log.error(e)
book_uuid = book.uuid book_uuid = book.uuid
metadata = { metadata = {

View File

@ -149,6 +149,20 @@ body {
word-wrap: break-word; word-wrap: break-word;
} }
#mainContent > canvas {
display: block;
margin-left: auto;
margin-right: auto;
}
.long-strip > .mainImage {
margin-bottom: 4px;
}
.long-strip > .mainImage:last-child {
margin-bottom: 0px !important;
}
#titlebar { #titlebar {
min-height: 25px; min-height: 25px;
height: auto; height: auto;

View File

@ -62,6 +62,7 @@ var currentImage = 0;
var imageFiles = []; var imageFiles = [];
var imageFilenames = []; var imageFilenames = [];
var totalImages = 0; var totalImages = 0;
var prevScrollPosition = 0;
var settings = { var settings = {
hflip: false, hflip: false,
@ -70,8 +71,8 @@ var settings = {
fitMode: kthoom.Key.B, fitMode: kthoom.Key.B,
theme: "light", theme: "light",
direction: 0, // 0 = Left to Right, 1 = Right to Left direction: 0, // 0 = Left to Right, 1 = Right to Left
nextPage: 0, // 0 = Reset to Top, 1 = Remember Position scrollbar: 1, // 0 = Hide Scrollbar, 1 = Show Scrollbar
scrollbar: 1 // 0 = Hide Scrollbar, 1 = Show Scrollbar pageDisplay: 0 // 0 = Single Page, 1 = Long Strip
}; };
kthoom.saveSettings = function() { kthoom.saveSettings = function() {
@ -187,7 +188,6 @@ function initProgressClick() {
} }
function loadFromArrayBuffer(ab) { function loadFromArrayBuffer(ab) {
var lastCompletion = 0;
const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' }); const collator = new Intl.Collator('en', { numeric: true, sensitivity: 'base' });
loadArchiveFormats(['rar', 'zip', 'tar'], function() { loadArchiveFormats(['rar', 'zip', 'tar'], function() {
// Open the file as an archive // Open the file as an archive
@ -216,9 +216,13 @@ function loadFromArrayBuffer(ab) {
"</a>" + "</a>" +
"</li>" "</li>"
); );
drawCanvas();
setImage(test.dataURI, null);
// display first page if we haven't yet // display first page if we haven't yet
if (imageFiles.length === currentImage + 1) { if (imageFiles.length === currentImage + 1) {
updatePage(lastCompletion); updatePage();
} }
} else { } else {
totalImages--; totalImages--;
@ -233,13 +237,6 @@ function loadFromArrayBuffer(ab) {
} }
function scrollTocToActive() { function scrollTocToActive() {
// Scroll to the thumbnail in the TOC on page change
$("#tocView").stop().animate({
scrollTop: $("#tocView a.active").position().top
}, 200);
}
function updatePage() {
$(".page").text((currentImage + 1 ) + "/" + totalImages); $(".page").text((currentImage + 1 ) + "/" + totalImages);
// Mark the current page in the TOC // Mark the current page in the TOC
@ -251,22 +248,40 @@ function updatePage() {
// Set it to active // Set it to active
.addClass("active"); .addClass("active");
// Scroll to the thumbnail in the TOC on page change
$("#tocView").stop().animate({
scrollTop: $("#tocView a.active").position().top
}, 200);
}
function updatePage() {
scrollTocToActive(); scrollTocToActive();
scrollCurrentImageIntoView();
updateProgress(); updateProgress();
pageDisplayUpdate();
if (imageFiles[currentImage]) { setTheme();
setImage(imageFiles[currentImage].dataURI);
} else {
setImage("loading");
}
$("body").toggleClass("dark-theme", settings.theme === "dark");
$("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0);
kthoom.setSettings(); kthoom.setSettings();
kthoom.saveSettings(); kthoom.saveSettings();
} }
function setTheme() {
$("body").toggleClass("dark-theme", settings.theme === "dark");
$("#mainContent").toggleClass("disabled-scrollbar", settings.scrollbar === 0);
}
function pageDisplayUpdate() {
if(settings.pageDisplay === 0) {
$(".mainImage").addClass("hide");
$(".mainImage").eq(currentImage).removeClass("hide");
$("#mainContent").removeClass("long-strip");
} else {
$(".mainImage").removeClass("hide");
$("#mainContent").addClass("long-strip");
scrollCurrentImageIntoView();
}
}
function updateProgress(loadPercentage) { function updateProgress(loadPercentage) {
if (settings.direction === 0) { if (settings.direction === 0) {
$("#progress .bar-read") $("#progress .bar-read")
@ -298,100 +313,93 @@ function updateProgress(loadPercentage) {
$("#progress .bar-read").css({ width: totalImages === 0 ? 0 : Math.round((currentImage + 1) / totalImages * 100) + "%"}); $("#progress .bar-read").css({ width: totalImages === 0 ? 0 : Math.round((currentImage + 1) / totalImages * 100) + "%"});
} }
function setImage(url) { function setImage(url, _canvas) {
var canvas = $("#mainImage")[0]; var canvas = _canvas || $(".mainImage").slice(-1)[0]; // Select the last item on the array if _canvas is null
var x = $("#mainImage")[0].getContext("2d"); var x = canvas.getContext("2d");
$("#mainText").hide(); $("#mainText").hide();
if (url === "loading") { if (url === "error") {
updateScale(true);
canvas.width = innerWidth - 100;
canvas.height = 200;
x.fillStyle = "black"; x.fillStyle = "black";
x.textAlign = "center"; x.textAlign = "center";
x.font = "24px sans-serif"; x.font = "24px sans-serif";
x.strokeStyle = "black"; x.strokeStyle = (settings.theme === "dark") ? "white" : "black";
x.fillText("Loading Page #" + (currentImage + 1), innerWidth / 2, 100); x.fillText("Unable to decompress image #" + (currentImage + 1), innerWidth / 2, 100);
$(".mainImage").slice(-1).addClass("error");
} else { } else {
if (url === "error") { if ($("body").css("scrollHeight") / innerHeight > 1) {
updateScale(true); $("body").css("overflowY", "scroll");
canvas.width = innerWidth - 100;
canvas.height = 200;
x.fillStyle = "black";
x.textAlign = "center";
x.font = "24px sans-serif";
x.strokeStyle = "black";
x.fillText("Unable to decompress image #" + (currentImage + 1), innerWidth / 2, 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 = "black";
x.font = "50px sans-serif";
x.strokeStyle = "black";
x.fillText("Page #" + (currentImage + 1) + " (" +
imageFiles[currentImage].filename + ")", innerWidth / 2, 100);
x.fillStyle = "black";
x.fillText("Is corrupt or not an image", innerWidth / 2, 200);
var xhr = new XMLHttpRequest();
if (/(html|htm)$/.test(imageFiles[currentImage].filename)) {
xhr.open("GET", url, true);
xhr.onload = function() {
$("#mainText").css("display", "");
$("#mainText").innerHTML("<iframe style=\"width:100%;height:700px;border:0\" src=\"data:text/html," + escape(xhr.responseText) + "\"></iframe>");
};
xhr.send(null);
} else if (!/(jpg|jpeg|png|gif|webp)$/.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;
settings.rotateTimes = (4 + settings.rotateTimes) % 4;
x.save();
if (settings.rotateTimes % 2 === 1) {
sh = w;
sw = h;
}
canvas.height = sh;
canvas.width = sw;
x.translate(sw / 2, sh / 2);
x.rotate(Math.PI / 2 * settings.rotateTimes);
x.translate(-w / 2, -h / 2);
if (settings.vflip) {
x.scale(1, -1);
x.translate(0, -h);
}
if (settings.hflip) {
x.scale(-1, 1);
x.translate(-w, 0);
}
canvas.style.display = "none";
scrollTo(0, 0);
x.drawImage(img, 0, 0);
updateScale(false);
canvas.style.display = "";
$("body").css("overflowY", "");
x.restore();
};
img.src = url;
} }
var img = new Image();
img.onerror = function() {
canvas.width = innerWidth - 100;
canvas.height = 300;
x.fillStyle = "black";
x.font = "50px sans-serif";
x.strokeStyle = "black";
x.fillText("Page #" + (currentImage + 1) + " (" +
imageFiles[currentImage].filename + ")", innerWidth / 2, 100);
x.fillStyle = "black";
x.fillText("Is corrupt or not an image", innerWidth / 2, 200);
var xhr = new XMLHttpRequest();
if (/(html|htm)$/.test(imageFiles[currentImage].filename)) {
xhr.open("GET", url, true);
xhr.onload = function() {
$("#mainText").css("display", "");
$("#mainText").innerHTML("<iframe style=\"width:100%;height:700px;border:0\" src=\"data:text/html," + escape(xhr.responseText) + "\"></iframe>");
};
xhr.send(null);
} else if (!/(jpg|jpeg|png|gif|webp)$/.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;
settings.rotateTimes = (4 + settings.rotateTimes) % 4;
x.save();
if (settings.rotateTimes % 2 === 1) {
sh = w;
sw = h;
}
canvas.height = sh;
canvas.width = sw;
x.translate(sw / 2, sh / 2);
x.rotate(Math.PI / 2 * settings.rotateTimes);
x.translate(-w / 2, -h / 2);
if (settings.vflip) {
x.scale(1, -1);
x.translate(0, -h);
}
if (settings.hflip) {
x.scale(-1, 1);
x.translate(-w, 0);
}
canvas.style.display = "none";
scrollTo(0, 0);
x.drawImage(img, 0, 0);
canvas.style.display = "";
$("body").css("overflowY", "");
x.restore();
};
img.src = url;
}
}
// reloadImages is a slow process when multiple images are involved. Only used when rotating/mirroring
function reloadImages() {
for(i=0; i < imageFiles.length; i++) {
setImage(imageFiles[i].dataURI, $(".mainImage")[i]);
} }
} }
@ -418,9 +426,6 @@ function showPrevPage() {
currentImage++; currentImage++;
} else { } else {
updatePage(); updatePage();
if (settings.nextPage === 0) {
$("#mainContent").scrollTop(0);
}
} }
} }
@ -431,36 +436,53 @@ function showNextPage() {
currentImage--; currentImage--;
} else { } else {
updatePage(); updatePage();
if (settings.nextPage === 0) {
$("#mainContent").scrollTop(0);
}
} }
} }
function updateScale(clear) { function scrollCurrentImageIntoView() {
var mainImageStyle = getElem("mainImage").style; if(settings.pageDisplay == 0) {
mainImageStyle.width = ""; // This will scroll all the way up when Single Page is selected
mainImageStyle.height = ""; $("#mainContent").scrollTop(0);
mainImageStyle.maxWidth = ""; } else {
mainImageStyle.maxHeight = ""; // This will scroll to the image when Long Strip is selected
var maxheight = innerHeight - 50; $("#mainContent").stop().animate({
scrollTop: $(".mainImage").eq(currentImage).offset().top + $("#mainContent").scrollTop() - $("#mainContent").offset().top
if (!clear) { }, 200);
switch (settings.fitMode) {
case kthoom.Key.B:
mainImageStyle.maxWidth = "100%";
mainImageStyle.maxHeight = maxheight + "px";
break;
case kthoom.Key.H:
mainImageStyle.height = maxheight + "px";
break;
case kthoom.Key.W:
mainImageStyle.width = "100%";
break;
default:
break;
}
} }
}
function updateScale() {
var canvasArray = $("#mainContent > canvas");
var maxheight = innerHeight - 50;
canvasArray.css("width", "");
canvasArray.css("height", "");
canvasArray.css("maxWidth", "");
canvasArray.css("maxHeight", "");
if(settings.pageDisplay === 0) {
canvasArray.addClass("hide");
pageDisplayUpdate();
}
switch (settings.fitMode) {
case kthoom.Key.B:
canvasArray.css("maxWidth", "100%");
canvasArray.css("maxHeight", maxheight + "px");
break;
case kthoom.Key.H:
canvasArray.css("maxHeight", maxheight + "px");
break;
case kthoom.Key.W:
canvasArray.css("width", "100%");
break;
default:
break;
}
$("#mainContent > canvas.error").css("width", innerWidth - 100);
$("#mainContent > canvas.error").css("height", 200);
$("#mainContent").css({maxHeight: maxheight + 5}); $("#mainContent").css({maxHeight: maxheight + 5});
kthoom.setSettings(); kthoom.setSettings();
kthoom.saveSettings(); kthoom.saveSettings();
@ -477,6 +499,20 @@ function keyHandler(evt) {
if (hasModifier) break; if (hasModifier) break;
showRightPage(); showRightPage();
break; break;
case kthoom.Key.S:
if (hasModifier) break;
settings.pageDisplay = 0;
pageDisplayUpdate();
kthoom.setSettings();
kthoom.saveSettings();
break;
case kthoom.Key.O:
if (hasModifier) break;
settings.pageDisplay = 1;
pageDisplayUpdate();
kthoom.setSettings();
kthoom.saveSettings();
break;
case kthoom.Key.L: case kthoom.Key.L:
if (hasModifier) break; if (hasModifier) break;
settings.rotateTimes--; settings.rotateTimes--;
@ -484,6 +520,7 @@ function keyHandler(evt) {
settings.rotateTimes = 3; settings.rotateTimes = 3;
} }
updatePage(); updatePage();
reloadImages();
break; break;
case kthoom.Key.R: case kthoom.Key.R:
if (hasModifier) break; if (hasModifier) break;
@ -492,6 +529,7 @@ function keyHandler(evt) {
settings.rotateTimes = 0; settings.rotateTimes = 0;
} }
updatePage(); updatePage();
reloadImages();
break; break;
case kthoom.Key.F: case kthoom.Key.F:
if (hasModifier) break; if (hasModifier) break;
@ -507,26 +545,27 @@ function keyHandler(evt) {
settings.hflip = true; settings.hflip = true;
} }
updatePage(); updatePage();
reloadImages();
break; break;
case kthoom.Key.W: case kthoom.Key.W:
if (hasModifier) break; if (hasModifier) break;
settings.fitMode = kthoom.Key.W; settings.fitMode = kthoom.Key.W;
updateScale(false); updateScale();
break; break;
case kthoom.Key.H: case kthoom.Key.H:
if (hasModifier) break; if (hasModifier) break;
settings.fitMode = kthoom.Key.H; settings.fitMode = kthoom.Key.H;
updateScale(false); updateScale();
break; break;
case kthoom.Key.B: case kthoom.Key.B:
if (hasModifier) break; if (hasModifier) break;
settings.fitMode = kthoom.Key.B; settings.fitMode = kthoom.Key.B;
updateScale(false); updateScale();
break; break;
case kthoom.Key.N: case kthoom.Key.N:
if (hasModifier) break; if (hasModifier) break;
settings.fitMode = kthoom.Key.N; settings.fitMode = kthoom.Key.N;
updateScale(false); updateScale();
break; break;
case kthoom.Key.SPACE: case kthoom.Key.SPACE:
if (evt.shiftKey) { if (evt.shiftKey) {
@ -545,6 +584,43 @@ function keyHandler(evt) {
} }
} }
function drawCanvas() {
var maxheight = innerHeight - 50;
var canvasElement = $("<canvas></canvas>");
var x = canvasElement[0].getContext("2d");
canvasElement.addClass("mainImage");
switch (settings.fitMode) {
case kthoom.Key.B:
canvasElement.css("maxWidth", "100%");
canvasElement.css("maxHeight", maxheight + "px");
break;
case kthoom.Key.H:
canvasElement.css("maxHeight", maxheight + "px");
break;
case kthoom.Key.W:
canvasElement.css("width", "100%");
break;
default:
break;
}
if(settings.pageDisplay === 0) {
canvasElement.addClass("hide");
}
//Fill with Placeholder text. setImage will override this
canvasElement.width = innerWidth - 100;
canvasElement.height = 200;
x.fillStyle = "black";
x.textAlign = "center";
x.font = "24px sans-serif";
x.strokeStyle = (settings.theme === "dark") ? "white" : "black";
x.fillText("Loading Page #" + (currentImage + 1), innerWidth / 2, 100);
$("#mainContent").append(canvasElement);
}
function init(filename) { function init(filename) {
var request = new XMLHttpRequest(); var request = new XMLHttpRequest();
request.open("GET", filename); request.open("GET", filename);
@ -556,16 +632,17 @@ function init(filename) {
console.warn(request.statusText, request.responseText); console.warn(request.statusText, request.responseText);
} }
}); });
kthoom.loadSettings();
setTheme();
updateScale();
request.send(); request.send();
initProgressClick(); initProgressClick();
document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : ""; document.body.className += /AppleWebKit/.test(navigator.userAgent) ? " webkit" : "";
kthoom.loadSettings();
updateScale(true);
$(document).keydown(keyHandler); $(document).keydown(keyHandler);
$(window).resize(function() { $(window).resize(function() {
updateScale(false); updateScale();
}); });
// Open TOC menu // Open TOC menu
@ -596,28 +673,35 @@ function init(filename) {
value = /^\d+$/.test(value) ? parseInt(value) : value; value = /^\d+$/.test(value) ? parseInt(value) : value;
settings[this.name] = value; settings[this.name] = value;
if(["hflip", "vflip", "rotateTimes"].includes(this.name)) {
reloadImages();
} else if(this.name === "direction") {
return updateProgress();
}
updatePage(); updatePage();
updateScale(false); updateScale();
}); });
// Close modal // Close modal
$(".closer, .overlay").click(function() { $(".closer, .overlay").click(function() {
$(".md-show").removeClass("md-show"); $(".md-show").removeClass("md-show");
$("#mainContent").focus(); // focus back on the main container so you use up/down keys without having to click on it
}); });
// TOC thumbnail pagination // TOC thumbnail pagination
$("#thumbnails").on("click", "a", function() { $("#thumbnails").on("click", "a", function() {
currentImage = $(this).data("page") - 1; currentImage = $(this).data("page") - 1;
updatePage(); updatePage();
if (settings.nextPage === 0) {
$("#mainContent").scrollTop(0);
}
}); });
// Fullscreen mode // Fullscreen mode
if (typeof screenfull !== "undefined") { if (typeof screenfull !== "undefined") {
$("#fullscreen").click(function() { $("#fullscreen").click(function() {
screenfull.toggle($("#container")[0]); screenfull.toggle($("#container")[0]);
// Focus on main container so you can use up/down keys immediately after fullscreen
$("#mainContent").focus();
}); });
if (screenfull.raw) { if (screenfull.raw) {
@ -641,7 +725,7 @@ function init(filename) {
showRightPage(); showRightPage();
}, },
}); });
$("#mainImage").click(function(evt) { $(".mainImage").click(function(evt) {
// Firefox does not support offsetX/Y so we have to manually calculate // Firefox does not support offsetX/Y so we have to manually calculate
// where the user clicked in the image. // where the user clicked in the image.
var mainContentWidth = $("#mainContent").width(); var mainContentWidth = $("#mainContent").width();
@ -676,5 +760,37 @@ function init(filename) {
showRightPage(); showRightPage();
} }
}); });
// Scrolling up/down will update current image if a new image is into view (for Long Strip Display)
$("#mainContent").scroll(function(){
var scroll = $("#mainContent").scrollTop();
if(settings.pageDisplay === 0) {
// Don't trigger the scroll for Single Page
} else if(scroll > prevScrollPosition) {
//Scroll Down
if(currentImage + 1 < imageFiles.length) {
if(currentImageOffset(currentImage + 1) <= 1) {
currentImage++;
scrollTocToActive();
updateProgress();
}
}
} else {
//Scroll Up
if(currentImage - 1 > -1 ) {
if(currentImageOffset(currentImage - 1) >= 0) {
currentImage--;
scrollTocToActive();
updateProgress();
}
}
}
// Update scroll position
prevScrollPosition = scroll;
});
} }
function currentImageOffset(imageIndex) {
return $(".mainImage").eq(imageIndex).offset().top - $("#mainContent").position().top
}

View File

@ -76,7 +76,6 @@
<div id="mainContent" tabindex="-1"> <div id="mainContent" tabindex="-1">
<div id="mainText" style="display:none"></div> <div id="mainText" style="display:none"></div>
<canvas id="mainImage"></canvas>
</div> </div>
<div id="left" class="arrow" onclick="showLeftPage()"></div> <div id="left" class="arrow" onclick="showLeftPage()"></div>
<div id="right" class="arrow" onclick="showRightPage()"></div> <div id="right" class="arrow" onclick="showRightPage()"></div>
@ -94,6 +93,8 @@
<tbody> <tbody>
<tr><td id="prev_page_key">&larr;</td> <td>{{_('Previous Page')}}</td></tr> <tr><td id="prev_page_key">&larr;</td> <td>{{_('Previous Page')}}</td></tr>
<tr><td id="next_page_key">&rarr;</td> <td>{{_('Next Page')}}</td></tr> <tr><td id="next_page_key">&rarr;</td> <td>{{_('Next Page')}}</td></tr>
<tr><td>S</td> <td>{{_('Single Page Display')}}</td></tr>
<tr><td>O</td> <td>{{_('Long Strip Display')}}</td></tr>
<tr><td>B</td> <td>{{_('Scale to Best')}}</td></tr> <tr><td>B</td> <td>{{_('Scale to Best')}}</td></tr>
<tr><td>W</td> <td>{{_('Scale to Width')}}</td></tr> <tr><td>W</td> <td>{{_('Scale to Width')}}</td></tr>
<tr><td>H</td> <td>{{_('Scale to Height')}}</td></tr> <tr><td>H</td> <td>{{_('Scale to Height')}}</td></tr>
@ -121,6 +122,15 @@
</div> </div>
</td> </td>
</tr> </tr>
<tr>
<th>{{_('Display')}}:</th>
<td>
<div class="inputs">
<label for="singlePage"><input type="radio" id="singlePage" name="pageDisplay" value="0" /> {{_('Single Page')}}</label>
<label for="longStrip"><input type="radio" id="longStrip" name="pageDisplay" value="1" /> {{_('Long Strip')}}</label>
</div>
</td>
</tr>
<tr> <tr>
<th>{{_('Scale')}}:</th> <th>{{_('Scale')}}:</th>
<td> <td>
@ -160,15 +170,6 @@
<label for="rightToLeft"><input type="radio" id="rightToLeft" name="direction" value="1" /> {{_('Right to Left')}}</label> <label for="rightToLeft"><input type="radio" id="rightToLeft" name="direction" value="1" /> {{_('Right to Left')}}</label>
</div> </div>
</td> </td>
</tr>
<tr>
<th>{{_('Next Page')}}:</th>
<td>
<div class="inputs">
<label for="resetToTop"><input type="radio" id="resetToTop" name="nextPage" value="0" /> {{_('Reset to Top')}}</label>
<label for="rememberPosition"><input type="radio" id="rememberPosition" name="nextPage" value="1" /> {{_('Remember Position')}}</label>
</div>
</td>
</tr> </tr>
<tr> <tr>
<th>{{_('Scrollbar')}}:</th> <th>{{_('Scrollbar')}}:</th>

View File

@ -43,7 +43,9 @@ See https://github.com/adobe-type-tools/cmap-resources
<script type="text/javascript"> <script type="text/javascript">
window.addEventListener('webviewerloaded', function() { window.addEventListener('webviewerloaded', function() {
PDFViewerApplicationOptions.set('disableAutoFetch', true); PDFViewerApplicationOptions.set('disableAutoFetch', true);
PDFViewerApplicationOptions.set('disableRange', true); PDFViewerApplicationOptions.set('disableRange', false);
PDFViewerApplicationOptions.set('disableStream', true);
PDFViewerApplicationOptions.set('disablePreferences', true);
PDFViewerApplicationOptions.set('cMapUrl', "{{ url_for('static', filename='cmaps/') }}"); PDFViewerApplicationOptions.set('cMapUrl', "{{ url_for('static', filename='cmaps/') }}");
PDFViewerApplicationOptions.set('sidebarViewOnLoad', 0); PDFViewerApplicationOptions.set('sidebarViewOnLoad', 0);
PDFViewerApplicationOptions.set('imageResourcesPath', "{{ url_for('static', filename='css/images/') }}"); PDFViewerApplicationOptions.set('imageResourcesPath', "{{ url_for('static', filename='css/images/') }}");

View File

@ -1165,11 +1165,15 @@ def serve_book(book_id, book_format, anyname):
data = calibre_db.get_book_format(book_id, book_format.upper()) data = calibre_db.get_book_format(book_id, book_format.upper())
if not data: if not data:
return "File not in Database" return "File not in Database"
log.info('Serving book: %s', data.name) range_header = request.headers.get('Range', None)
if config.config_use_google_drive: if config.config_use_google_drive:
try: try:
headers = Headers() headers = Headers()
headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream") headers["Content-Type"] = mimetypes.types_map.get('.' + book_format, "application/octet-stream")
if not range_header:
log.info('Serving book: %s', data.name)
headers['Accept-Ranges'] = 'bytes'
df = getFileFromEbooksFolder(book.path, data.name + "." + book_format) df = getFileFromEbooksFolder(book.path, data.name + "." + book_format)
return do_gdrive_download(df, headers, (book_format.upper() == 'TXT')) return do_gdrive_download(df, headers, (book_format.upper() == 'TXT'))
except AttributeError as ex: except AttributeError as ex:
@ -1177,6 +1181,7 @@ def serve_book(book_id, book_format, anyname):
return "File Not Found" return "File Not Found"
else: else:
if book_format.upper() == 'TXT': if book_format.upper() == 'TXT':
log.info('Serving book: %s', data.name)
try: try:
rawdata = open(os.path.join(config.config_calibre_dir, book.path, data.name + "." + book_format), rawdata = open(os.path.join(config.config_calibre_dir, book.path, data.name + "." + book_format),
"rb").read() "rb").read()
@ -1186,7 +1191,13 @@ def serve_book(book_id, book_format, anyname):
except FileNotFoundError: except FileNotFoundError:
log.error("File Not Found") log.error("File Not Found")
return "File Not Found" return "File Not Found"
return send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format) # enable byte range read of pdf
response = make_response(
send_from_directory(os.path.join(config.config_calibre_dir, book.path), data.name + "." + book_format))
if not range_header:
log.info('Serving book: %s', data.name)
response.headers['Accept-Ranges'] = 'bytes'
return response
@web.route("/download/<int:book_id>/<book_format>", defaults={'anyname': 'None'}) @web.route("/download/<int:book_id>/<book_format>", defaults={'anyname': 'None'})