Skip to content

Commit

Permalink
Add support for importing KOReader & Calibre annotations (#4780)
Browse files Browse the repository at this point in the history
  • Loading branch information
AbeJellinek authored Nov 8, 2024
1 parent d3456b0 commit 9f8c5c8
Show file tree
Hide file tree
Showing 11 changed files with 494 additions and 3 deletions.
7 changes: 7 additions & 0 deletions chrome/content/zotero/reader.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<html:link rel="localization" href="browser/browserSets.ftl"/>
<html:link rel="localization" href="toolkit/global/textActions.ftl"/>
<html:link rel="localization" href="zotero.ftl" />
<html:link rel="localization" href="reader.ftl" />
</linkset>

<script type="application/javascript">
Expand Down Expand Up @@ -102,6 +103,12 @@
label="&zotero.pdfReader.transferFromPDF;"
oncommand="reader.transferFromPDF()"
/>
<menuitem
id="menu_importFromEPUB"
class="menu-type-reader epub"
data-l10n-id="pdfReader-import-from-epub"
oncommand="reader.importFromEPUB()"
/>
<menuseparator class="menu-type-reader pdf"/>
<menuitem class="menu-type-reader pdf" label="&zotero.general.saveAs;" oncommand="reader.export()"/>
<menuitem data-l10n-id="menu-print" oncommand="reader.print()"/>
Expand Down
1 change: 1 addition & 0 deletions chrome/content/zotero/xpcom/intl.js
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,7 @@ Zotero.Intl = new function () {
ftl = new Localization([
'branding/brand.ftl',
'zotero.ftl',
'reader.ftl',
// More FTL files can be hardcoded here, or added later with
// Zotero.ftl.addResourceIds(['...'])
], true);
Expand Down
171 changes: 169 additions & 2 deletions chrome/content/zotero/xpcom/reader.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ var { FilePicker } = ChromeUtils.importESModule('chrome://zotero/content/modules

const { BlockingObserver } = ChromeUtils.import("chrome://zotero/content/BlockingObserver.jsm");

const ZipReader = Components.Constructor(
"@mozilla.org/libjar/zip-reader;1",
"nsIZipReader",
"open"
);

// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Invalid_array_length
const ARRAYBUFFER_MAX_LENGTH = Services.appinfo.is64Bit
? Math.pow(2, 33)
Expand Down Expand Up @@ -738,6 +744,161 @@ class ReaderInstance {
}
}

/**
* @param {string} [path] For tests: used instead of getFilePathAsync()
* @returns {Promise<void>}
*/
async importFromEPUB(path = null) {
let getKOReaderInput = async (path) => {
// KOReader metadata is never embedded, so we just need to check
// ./[basename-without-.epub].sdr/metadata.epub.lua
if (path.endsWith('.epub')) {
path = PathUtils.join(path.slice(0, -5) + '.sdr', 'metadata.epub.lua');
}
else if (!path.endsWith('.lua')) {
return null;
}
if (!await IOUtils.exists(path)) {
return null;
}
return Cu.cloneInto(await IOUtils.read(path), this._iframeWindow);
};

let getCalibreInput = async (path) => {
let externalPath = PathUtils.filename(path).endsWith('.opf')
? path
: PathUtils.join(PathUtils.parent(path), 'metadata.opf');
if (await IOUtils.exists(externalPath)) {
return Zotero.File.getContentsAsync(externalPath);
}
if (!path.endsWith('.epub')) {
return null;
}

let epubZip;
try {
epubZip = new ZipReader(Zotero.File.pathToFile(path));
}
catch (e) {
Zotero.logError(e);
return null;
}

try {
const CALIBRE_BOOKMARKS_PATH = 'META-INF/calibre_bookmarks.txt';
if (!epubZip.hasEntry(CALIBRE_BOOKMARKS_PATH)) {
return null;
}
// Await before returning for the try-finally
return await Zotero.File.getContentsAsync(epubZip.getInputStream(CALIBRE_BOOKMARKS_PATH));
}
finally {
epubZip.close();
}
};

let selectFile = async () => {
let fp = new FilePicker();
fp.init(this._window, Zotero.ftl.formatValueSync('pdfReader-import-from-epub-prompt-title'), fp.modeOpen);
fp.appendFilter('EPUB Data', '*.epub; *.lua; *.opf');
if (await fp.show() !== fp.returnOK) {
return null;
}
return fp.file;
};

path ??= await this._item.getFilePathAsync();
let isOpenFile = true;
if (!path) {
path = await selectFile();
isOpenFile = false;
}
while (path) {
let koReaderInput;
try {
koReaderInput = await getKOReaderInput(path);
}
catch (e) {
Zotero.logError(e);
}

let calibreInput;
try {
calibreInput = await getCalibreInput(path);
}
catch (e) {
Zotero.logError(e);
}

let koReaderStats = koReaderInput && this._internalReader.getKOReaderAnnotationStats(koReaderInput);
let calibreStats = calibreInput && this._internalReader.getCalibreAnnotationStats(calibreInput);
let stats = koReaderStats || calibreStats || { count: 0 };

if (stats.count) {
let ps = Services.prompt;
let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL
+ ps.BUTTON_POS_2 * ps.BUTTON_TITLE_IS_STRING;
let index = ps.confirmEx(
this._window,
Zotero.ftl.formatValueSync('pdfReader-import-from-epub-prompt-title'),
Zotero.ftl.formatValueSync('pdfReader-import-from-epub-prompt-text', {
count: stats.count,
lastModifiedRelative: Zotero.Date.toRelativeDate(stats.lastModified),
tool: stats === koReaderStats ? 'KOReader' : 'Calibre',
}),
buttonFlags,
Zotero.getString('general.import'),
'',
Zotero.ftl.formatValueSync('pdfReader-import-from-epub-select-other'),
'', {}
);
if (index === 0) {
try {
if (stats === koReaderStats) {
this._internalReader.importAnnotationsFromKOReaderMetadata(koReaderInput);
}
else {
this._internalReader.importAnnotationsFromCalibreMetadata(calibreInput);
}
}
catch (e) {
Zotero.alert(this._window, Zotero.getString('general.error'), e.message);
}
break;
}
else if (index === 1) {
break;
}
}
else {
let ps = Services.prompt;
let buttonFlags = ps.BUTTON_POS_0 * ps.BUTTON_TITLE_IS_STRING
+ ps.BUTTON_POS_1 * ps.BUTTON_TITLE_CANCEL;

let message = isOpenFile
? Zotero.ftl.formatValueSync('pdfReader-import-from-epub-no-annotations-current-file')
: Zotero.ftl.formatValueSync('pdfReader-import-from-epub-no-annotations-other-file', {
filename: PathUtils.filename(path)
});
let index = ps.confirmEx(
this._window,
Zotero.ftl.formatValueSync('pdfReader-import-from-epub-prompt-title'),
message,
buttonFlags,
Zotero.ftl.formatValueSync('pdfReader-import-from-epub-select-other'),
'', '', '', {}
);
if (index === 1) {
break;
}
}

path = await selectFile();
isOpenFile = false;
}
}

export() {
let zp = Zotero.getActiveZoteroPane();
zp.exportPDF(this._item.id);
Expand Down Expand Up @@ -1294,16 +1455,22 @@ class ReaderWindow extends ReaderInstance {
_onFileMenuOpen() {
let item = Zotero.Items.get(this._item.id);
let library = Zotero.Libraries.get(item.libraryID);

let transferFromPDFMenuitem = this._window.document.getElementById('menu_transferFromPDF');
let importFromEPUBMenuitem = this._window.document.getElementById('menu_importFromEPUB');

if (item
&& library.filesEditable
&& library.editable
&& !(item.deleted || item.parentItem && item.parentItem.deleted)) {
let annotations = item.getAnnotations();
let canTransferFromPDF = annotations.find(x => x.annotationIsExternal);
this._window.document.getElementById('menu_transferFromPDF').setAttribute('disabled', !canTransferFromPDF);
transferFromPDFMenuitem.setAttribute('disabled', !canTransferFromPDF);
importFromEPUBMenuitem.setAttribute('disabled', false);
}
else {
this._window.document.getElementById('menu_transferFromPDF').setAttribute('disabled', true);
transferFromPDFMenuitem.setAttribute('disabled', true);
importFromEPUBMenuitem.setAttribute('disabled', true);
}
}

Expand Down
8 changes: 8 additions & 0 deletions chrome/content/zotero/zoteroPane.xhtml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@
<linkset>
<html:link rel="localization" href="browser/menubar.ftl"/>
<html:link rel="localization" href="browser/browserSets.ftl"/>
<html:link rel="localization" href="zotero.ftl"/>
<html:link rel="localization" href="reader.ftl"/>
</linkset>

<script>
Expand Down Expand Up @@ -305,6 +307,12 @@
label="&zotero.pdfReader.transferFromPDF;"
oncommand="ZoteroStandalone.currentReader.transferFromPDF()"
/>
<menuitem
id="menu_importFromEPUB"
class="menu-type-reader epub"
data-l10n-id="pdfReader-import-from-epub"
oncommand="ZoteroStandalone.currentReader.importFromEPUB()"
/>
<menuseparator class="menu-type-reader pdf"/>
<menuitem id="menu_export_file" class="menu-type-reader pdf"
label="&zotero.general.saveAs;"
Expand Down
20 changes: 20 additions & 0 deletions chrome/locale/en-US/zotero/reader.ftl
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,23 @@ pdfReader-findInDocumentInput =
.title = Find
.placeholder = { pdfReader-find-in-document }
.aria-description = To turn a search result into a highlight annotation, press { general-key-control }-{ option-or-alt }-1. To turn a search result into an underline annotation, press { general-key-control }-{ option-or-alt }-2.
pdfReader-import-from-epub =
.label = Import Ebook Annotations…
pdfReader-import-from-epub-prompt-title = Import Ebook Annotations
pdfReader-import-from-epub-prompt-text =
{ -app-name } found { $count ->
[1] { $count } { $tool } annotation
*[other] { $count } { $tool } annotations
}, last edited { $lastModifiedRelative }.
Any { -app-name } annotations that were previously imported from this ebook will be updated.
pdfReader-import-from-epub-no-annotations-current-file =
This ebook does not appear to contain any importable annotations.
{ -app-name } can import ebook annotations created in Calibre and KOReader.
pdfReader-import-from-epub-no-annotations-other-file =
{ $filename }” does not appear to contain any Calibre or KOReader annotations.
If this ebook has been annotated with KOReader, try selecting a “metadata.epub.lua” file directly.
pdfReader-import-from-epub-select-other = Select Other File…
Binary file added test/tests/data/moby_dick/book.epub
Binary file not shown.
118 changes: 118 additions & 0 deletions test/tests/data/moby_dick/book.sdr/metadata.epub.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
-- /home/user/Downloads/herman-melville_moby-dick.sdr/metadata.epub.lua
return {
["annotations"] = {
[1] = {
["chapter"] = "Extracts",
["datetime"] = "2024-10-23 09:28:47",
["drawer"] = "lighten",
["note"] = "this is a test note",
["page"] = "/body/DocFragment[5]/body/section/section/blockquote[9]/p/text().4",
["pageno"] = 10,
["pos0"] = "/body/DocFragment[5]/body/section/section/blockquote[9]/p/text().4",
["pos1"] = "/body/DocFragment[5]/body/section/section/blockquote[9]/p/text().119",
["text"] = "visited this country also with a view of catching horse-whales, which had bones of very great value for their teeth",
},
[2] = {
["chapter"] = "Extracts",
["datetime"] = "2024-10-23 09:28:41",
["drawer"] = "lighten",
["page"] = "/body/DocFragment[5]/body/section/section/blockquote[9]/cite/text()[1].50",
["pageno"] = 10,
["pos0"] = "/body/DocFragment[5]/body/section/section/blockquote[9]/cite/text()[1].50",
["pos1"] = "/body/DocFragment[5]/body/section/section/blockquote[9]/cite/text()[1].75",
["text"] = "his mouth by King Alfred",
},
},
["cache_file_path"] = "/home/user/.config/koreader/cache/cr3cache/herman-melvi-by-dick.epub.a2cc4d69.1.cr3",
["config_panel_index"] = 1,
["copt_b_page_margin"] = 15,
["copt_block_rendering_mode"] = 3,
["copt_cjk_width_scaling"] = 100,
["copt_embedded_css"] = 1,
["copt_embedded_fonts"] = 1,
["copt_font_base_weight"] = 0,
["copt_font_gamma"] = 15,
["copt_font_hinting"] = 2,
["copt_font_kerning"] = 3,
["copt_font_size"] = 22,
["copt_h_page_margins"] = {
[1] = 10,
[2] = 10,
},
["copt_line_spacing"] = 100,
["copt_nightmode_images"] = 1,
["copt_render_dpi"] = 96,
["copt_rotation_mode"] = 0,
["copt_smooth_scaling"] = 0,
["copt_status_line"] = 1,
["copt_sync_t_b_page_margins"] = 0,
["copt_t_page_margin"] = 15,
["copt_view_mode"] = 0,
["copt_visible_pages"] = 1,
["copt_word_expansion"] = 0,
["copt_word_spacing"] = {
[1] = 95,
[2] = 75,
},
["cre_dom_version"] = 20240114,
["css"] = "./data/epub.css",
["doc_pages"] = 1068,
["doc_path"] = "/home/user/Downloads/herman-melville_moby-dick.epub",
["doc_props"] = {
["authors"] = "Herman Melville",
["description"] = "Captain Ahab, having lost his leg to the white whale Moby Dick, travels the world on a quest for vengeance.",
["identifiers"] = "url:https://standardebooks.org/ebooks/herman-melville/moby-dick",
["keywords"] = "Whaling -- Fiction\
Sea stories\
Psychological fiction\
Ship captains -- Fiction\
Adventure stories\
Mentally ill -- Fiction\
Ahab, Captain (Fictitious character) -- Fiction\
Whales -- Fiction\
Whaling ships -- Fiction",
["language"] = "en-US",
["series"] = "The Guardian’s Best 100 Novels in English (2015)",
["series_index"] = 17,
["title"] = "Moby Dick",
},
["floating_punctuation"] = 0,
["font_face"] = "Noto Serif",
["font_family_fonts"] = {},
["handmade_flows_edit_enabled"] = true,
["handmade_flows_enabled"] = false,
["handmade_toc_edit_enabled"] = true,
["handmade_toc_enabled"] = false,
["hide_nonlinear_flows"] = false,
["highlight_drawer"] = "lighten",
["hyph_force_algorithmic"] = false,
["hyph_soft_hyphens_only"] = false,
["hyph_trust_soft_hyphens"] = false,
["hyphenation"] = true,
["inverse_reading_order"] = false,
["last_xpointer"] = "/body/DocFragment[5]/body/section/section/blockquote[7]/p/text().42",
["page_overlap_style"] = "dim",
["partial_md5_checksum"] = "24f29131f720f7c89d54476baece3657",
["partial_rerendering"] = true,
["percent_finished"] = 0.0093632958801498,
["preferred_dictionaries"] = {},
["readermenu_tab_index"] = 3,
["show_overlap_enable"] = false,
["stats"] = {
["authors"] = "Herman Melville",
["highlights"] = 1,
["language"] = "en-US",
["notes"] = 1,
["pages"] = 1068,
["performance_in_pages"] = {},
["series"] = "The Guardian’s Best 100 Novels in English (2015) #17",
["title"] = "Moby Dick",
},
["summary"] = {
["modified"] = "2024-10-23",
["status"] = "reading",
},
["text_lang"] = "en-US",
["text_lang_embedded_langs"] = true,
["toc_ticks_ignored_levels"] = {},
}
Loading

0 comments on commit 9f8c5c8

Please sign in to comment.