zotero/chrome/content/zotero/xpcom/annotations.js
2022-09-13 17:16:08 +03:00

343 lines
11 KiB
JavaScript

/*
***** BEGIN LICENSE BLOCK *****
Copyright © 2020 Corporation for Digital Scholarship
Vienna, Virginia, USA
https://www.zotero.org
This file is part of Zotero.
Zotero is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
Zotero is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with Zotero. If not, see <http://www.gnu.org/licenses/>.
***** END LICENSE BLOCK *****
*/
"use strict";
Zotero.Annotations = new function () {
Zotero.defineProperty(this, 'ANNOTATION_POSITION_MAX_SIZE', { value: 65000 });
// Keep in sync with items.js::loadAnnotations()
Zotero.defineProperty(this, 'ANNOTATION_TYPE_HIGHLIGHT', { value: 1 });
Zotero.defineProperty(this, 'ANNOTATION_TYPE_NOTE', { value: 2 });
Zotero.defineProperty(this, 'ANNOTATION_TYPE_IMAGE', { value: 3 });
Zotero.defineProperty(this, 'ANNOTATION_TYPE_INK', { value: 4 });
Zotero.defineProperty(this, 'PROPS', {
value: ['type', 'authorName', 'text', 'comment', 'color', 'pageLabel', 'sortIndex', 'position'],
writable: false
});
this.getCacheImagePath = function ({ libraryID, key }) {
var file = this._getLibraryCacheDirectory(libraryID);
return OS.Path.join(file, key + '.png');
};
this.hasCacheImage = async function (item) {
return OS.File.exists(this.getCacheImagePath(item));
};
this.saveCacheImage = async function ({ libraryID, key }, blob) {
var item = await Zotero.Items.getByLibraryAndKeyAsync(libraryID, key);
if (!item) {
throw new Error(`Item not found`);
}
if (item.itemType != 'annotation' || !['image', 'ink'].includes(item.annotationType)) {
throw new Error("Item must be an image/ink annotation item");
}
var cacheDir = Zotero.DataDirectory.getSubdirectory('cache', true);
var file = this._getLibraryCacheDirectory(item.libraryID);
await Zotero.File.createDirectoryIfMissingAsync(file, { from: cacheDir });
file = OS.Path.join(file, item.key + '.png');
Zotero.debug("Creating annotation cache file " + file);
await Zotero.File.putContentsAsync(file, blob);
await Zotero.File.setNormalFilePermissions(file);
return file;
};
this.removeCacheImage = async function ({ libraryID, key }) {
var path = this.getCacheImagePath({ libraryID, key });
Zotero.debug("Deleting annotation cache file " + path);
await OS.File.remove(path, { ignoreAbsent: true });
};
/**
* Remove cache files that are no longer in use
*/
this.removeOrphanedCacheFiles = async function () {
// TODO
};
/**
* Remove all cache files for a given library
*/
this.removeLibraryCacheFiles = async function (libraryID) {
var path = this._getLibraryCacheDirectory(libraryID);
await OS.File.removeDir(path, { ignoreAbsent: true, ignorePermissions: true });
};
this._getLibraryCacheDirectory = function (libraryID) {
var parts = [Zotero.DataDirectory.getSubdirectory('cache')];
var library = Zotero.Libraries.get(libraryID);
if (library.libraryType == 'user') {
parts.push('library');
}
else if (library.libraryType == 'group') {
parts.push('groups', library.groupID + '');
}
else {
throw new Error(`Unexpected library type '${library.libraryType}'`);
}
return OS.Path.join(...parts);
};
this.toJSON = async function (item) {
var o = {};
o.libraryID = item.libraryID;
o.key = item.key;
o.type = item.annotationType;
o.isExternal = item.annotationIsExternal;
var isAuthor = !item.createdByUserID || item.createdByUserID == Zotero.Users.getCurrentUserID();
var isGroup = item.library.libraryType == 'group';
if (item.annotationAuthorName) {
o.authorName = item.annotationAuthorName;
if (isGroup) {
o.lastModifiedByUser = Zotero.Users.getName(item.lastModifiedByUserID)
|| Zotero.Users.getName(item.createdByUserID);
}
}
else if (!o.isExternal && isGroup) {
o.authorName = Zotero.Users.getName(item.createdByUserID);
o.isAuthorNameAuthoritative = true;
if (item.lastModifiedByUserID) {
o.lastModifiedByUser = Zotero.Users.getName(item.lastModifiedByUserID);
}
}
o.readOnly = o.isExternal || !isAuthor;
if (o.type == 'highlight') {
o.text = item.annotationText;
}
else if (['image', 'ink'].includes(o.type)) {
let file = this.getCacheImagePath(item);
if (await OS.File.exists(file)) {
o.image = await Zotero.File.generateDataURI(file, 'image/png');
}
}
o.comment = item.annotationComment;
o.pageLabel = item.annotationPageLabel;
o.color = item.annotationColor;
o.sortIndex = item.annotationSortIndex;
// annotationPosition is a JSON string, but we want to pass the raw object to the reader
o.position = JSON.parse(item.annotationPosition);
// Add tags and tag colors
var tagColors = Zotero.Tags.getColors(item.libraryID);
var tags = item.getTags().map((t) => {
let obj = {
name: t.tag
};
if (tagColors.has(t.tag)) {
obj.color = tagColors.get(t.tag).color;
// Add 'position' for sorting
obj.position = tagColors.get(t.tag).position;
}
return obj;
});
// Sort colored tags by position and other tags by name
tags.sort((a, b) => {
if (!a.color && !b.color) return Zotero.localeCompare(a.name, b.name);
if (!a.color && !b.color) return -1;
if (!a.color && b.color) return 1;
return a.position - b.position;
});
// Remove temporary 'position' value
tags.forEach(t => delete t.position);
if (tags.length) {
o.tags = tags;
}
o.dateModified = Zotero.Date.sqlToISO8601(item.dateModified);
return o;
};
/**
* @param {Zotero.Item} attachment - Saved parent attachment item
* @param {Object} json
* @return {Promise<Zotero.Item>} - Promise for an annotation item
*/
this.saveFromJSON = async function (attachment, json, saveOptions = {}) {
if (!attachment) {
throw new Error("'attachment' not provided");
}
if (!attachment.libraryID) {
throw new Error("'attachment' is not saved");
}
if (!json.key) {
throw new Error("'key' not provided in JSON");
}
var item = Zotero.Items.getByLibraryAndKey(attachment.libraryID, json.key);
if (!item) {
item = new Zotero.Item('annotation');
item.libraryID = attachment.libraryID;
item.key = json.key;
await item.loadPrimaryData();
}
item.parentID = attachment.id;
item._requireData('annotation');
item._requireData('annotationDeferred');
item.annotationType = json.type;
item.annotationAuthorName = json.authorName || '';
if (json.type == 'highlight') {
item.annotationText = json.text;
}
item.annotationIsExternal = !!json.isExternal;
item.annotationComment = json.comment;
item.annotationColor = json.color;
item.annotationPageLabel = json.pageLabel;
item.annotationSortIndex = json.sortIndex;
item.annotationPosition = JSON.stringify(Object.assign({}, json.position));
// TODO: Can colors be set?
item.setTags((json.tags || []).map(t => ({ tag: t.name })));
// For Mendeley import -- additive only
if (json.relations) {
for (let predicate in json.relations) {
item.addRelation(predicate, json.relations[predicate]);
}
}
await item.saveTx(saveOptions);
return item;
};
/**
* Split annotation if position exceed the limit
*
* @param {Object} annotation
* @returns {Array<Object>} annotations
*/
this.splitAnnotationJSON = function (annotation) {
let splitAnnotations = [];
let tmpAnnotation = null;
let totalLength = 0;
if (annotation.position.rects) {
for (let i = 0; i < annotation.position.rects.length; i++) {
let rect = annotation.position.rects[i];
if (!tmpAnnotation) {
tmpAnnotation = JSON.parse(JSON.stringify(annotation));
tmpAnnotation.key = Zotero.DataObjectUtilities.generateKey();
tmpAnnotation.position.rects = [];
totalLength = JSON.stringify(tmpAnnotation.position).length;
}
// [],
let length = rect.join(',').length + 3;
if (totalLength + length <= this.ANNOTATION_POSITION_MAX_SIZE) {
tmpAnnotation.position.rects.push(rect);
totalLength += length;
}
else if (!tmpAnnotation.position.rects.length) {
throw new Error(`Cannot fit single 'rect' into 'position'`);
}
else {
splitAnnotations.push(tmpAnnotation);
tmpAnnotation = null;
i--;
}
}
if (tmpAnnotation) {
splitAnnotations.push(tmpAnnotation);
}
}
else if (annotation.position.paths) {
for (let i = 0; i < annotation.position.paths.length; i++) {
let path = annotation.position.paths[i];
for (let j = 0; j < path.length; j += 2) {
if (!tmpAnnotation) {
tmpAnnotation = JSON.parse(JSON.stringify(annotation));
tmpAnnotation.key = Zotero.DataObjectUtilities.generateKey();
tmpAnnotation.position.paths = [[]];
totalLength = JSON.stringify(tmpAnnotation.position).length;
}
let point = [path[j], path[j + 1]];
// 1,2,
let length = point.join(',').length + 1;
if (totalLength + length <= this.ANNOTATION_POSITION_MAX_SIZE) {
tmpAnnotation.position.paths[tmpAnnotation.position.paths.length - 1].push(...point);
totalLength += length;
}
else if (tmpAnnotation.position.paths.length === 1
&& !tmpAnnotation.position.paths[tmpAnnotation.position.paths.length - 1].length) {
throw new Error(`Cannot fit single point into 'position'`);
}
else {
splitAnnotations.push(tmpAnnotation);
tmpAnnotation = null;
j -= 2;
}
}
// If not the last path
if (i !== annotation.position.paths.length - 1) {
// [],
totalLength += 3;
tmpAnnotation.position.paths.push([]);
}
}
if (tmpAnnotation) {
splitAnnotations.push(tmpAnnotation);
}
}
return splitAnnotations;
};
/**
* Split annotations
*
* @param {Zotero.Item[]} items
* @returns {Promise<void>}
*/
this.splitAnnotations = async function (items) {
if (!Array.isArray(items)) {
items = [items];
}
if (!items.every(item => item.isAnnotation())) {
throw new Error('All items must be annotations');
}
for (let item of items) {
if (item.annotationPosition.length <= this.ANNOTATION_POSITION_MAX_SIZE) {
continue;
}
let annotation = await this.toJSON(item);
let splitAnnotations = this.splitAnnotationJSON(annotation);
for (let splitAnnotation of splitAnnotations) {
await this.saveFromJSON(item.parentItem, splitAnnotation);
}
await item.eraseTx();
}
};
};