Skip to content

Commit

Permalink
EPUB/snapshot image annotations
Browse files Browse the repository at this point in the history
  • Loading branch information
AbeJellinek committed Sep 19, 2023
1 parent 85c5595 commit 373db57
Show file tree
Hide file tree
Showing 10 changed files with 354 additions and 235 deletions.
165 changes: 1 addition & 164 deletions demo/snapshot/annotations.js
Original file line number Diff line number Diff line change
@@ -1,164 +1 @@
export default [
{
type: "highlight",
color: "#5fb236",
sortIndex: "00008186",
position: {
type: "CssSelector",
value: "#the_zotero_pdf_reader_and_note_editor"
},
text: "The Zotero PDF Reader and Note Editor",
pageLabel: "",
comment: "interesting!",
tags: [],
id: "ZVQG2R4M",
dateCreated: "2023-05-09T17:05:57.843Z",
dateModified: "2023-06-23T19:00:02.837Z",
authorName: "John",
isAuthorNameAuthoritative: true
},
{
type: "note",
color: "#ffd400",
sortIndex: "00008353",
position: {
type: "CssSelector",
value: "div:nth-child(3) > div:first-of-type > p:last-child"
},
pageLabel: "",
comment: "Good screenshot",
tags: [],
id: "UIRHN27J",
dateCreated: "2023-05-09T17:05:02.325Z",
dateModified: "2023-06-23T19:00:02.838Z",
authorName: "John",
isAuthorNameAuthoritative: true
},
{
type: "note",
color: "#ffd400",
sortIndex: "00008353",
position: {
type: "CssSelector",
value: "div:nth-child(3) > div:first-of-type > p:last-child"
},
pageLabel: "",
comment: "To do: read that paper",
tags: [],
id: "NQV7SU47",
dateCreated: "2023-05-09T17:11:35.478Z",
dateModified: "2023-06-23T19:00:02.838Z",
authorName: "John",
isAuthorNameAuthoritative: true
},
{
type: "highlight",
color: "#2ea8e5",
sortIndex: "00009678",
position: {
type: "CssSelector",
value: "#content > div > div:first-child > div:nth-child(3)",
refinedBy: {
type: "TextPositionSelector",
start: 1536,
end: 1705
}
},
text: "In the items list\n\n\nYou can create a note from all annotations in a PDF by right-clicking on the parent item in the items list and choosing Add Note from Annotations.\n\n\n",
pageLabel: "",
comment: "",
tags: [],
id: "73N2RIZ5",
dateCreated: "2023-05-09T17:05:30.346Z",
dateModified: "2023-06-23T19:00:02.838Z",
authorName: "John",
isAuthorNameAuthoritative: true
},
{
type: "underline",
color: "#2ea8e5",
sortIndex: "00011104",
position: {
type: "CssSelector",
value: "div:nth-child(18) > p:first-child",
refinedBy: {
type: "TextPositionSelector",
start: 42,
end: 62
}
},
text: "click the “…” button",
pageLabel: "",
comment: "",
tags: [],
id: "N4IFWQZX",
dateCreated: "2023-06-23T18:59:44.374Z",
dateModified: "2023-06-23T19:00:02.838Z",
authorName: "John",
isAuthorNameAuthoritative: true
},
{
type: "highlight",
color: "#a28ae5",
sortIndex: "00012053",
position: {
type: "CssSelector",
value: "div:nth-child(3) > div:last-child > p:last-child",
refinedBy: {
type: "TextPositionSelector",
start: 1,
end: 91
}
},
text: "Note that annotations created in the built-in PDF reader are stored in the Zotero database",
pageLabel: "",
comment: "good to know",
tags: [],
id: "MVMY3NWI",
dateCreated: "2023-05-09T17:05:16.087Z",
dateModified: "2023-06-23T19:00:02.838Z",
authorName: "John",
isAuthorNameAuthoritative: true
},
{
type: "underline",
color: "#f19837",
sortIndex: "00012284",
position: {
type: "CssSelector",
value: "#dw__toc > h3"
},
text: "Table of Contents",
pageLabel: "",
comment: "Come back to this",
tags: [],
id: "5RAMSIAA",
dateCreated: "2023-06-23T18:59:31.824Z",
dateModified: "2023-06-23T19:00:02.838Z",
authorName: "John",
isAuthorNameAuthoritative: true
},
{
type: "highlight",
color: "#ff6666",
sortIndex: "00012596",
position: {
type: "CssSelector",
value: "div:first-child > div:last-child > div:first-child > div:last-child",
refinedBy: {
type: "TextPositionSelector",
start: 19,
end: 63
}
},
text: "Last modified: 2022/04/08 04:25 by dstillman",
pageLabel: "",
comment: "",
tags: [],
id: "RL69GNWI",
dateCreated: "2023-05-09T17:05:51.707Z",
dateModified: "2023-06-23T19:00:02.838Z",
authorName: "John",
isAuthorNameAuthoritative: true
}
];
export default [];
20 changes: 9 additions & 11 deletions src/common/components/toolbar.js
Original file line number Diff line number Diff line change
Expand Up @@ -175,17 +175,15 @@ function Toolbar(props) {
<span className="button-background"/>
</button>
)}
{props.type === 'pdf' && (
<button
tabIndex={-1}
className={cx('toolbarButton area', { toggled: props.tool.type === 'image' })}
title={intl.formatMessage({ id: 'pdfReader.selectArea' })}
disabled={props.readOnly}
onClick={() => handleToolClick('image')}
>
<span className="button-background"/>
</button>
)}
<button
tabIndex={-1}
className={cx('toolbarButton area', { toggled: props.tool.type === 'image' })}
title={intl.formatMessage({ id: 'pdfReader.selectArea' })}
disabled={props.readOnly}
onClick={() => handleToolClick('image')}
>
<span className="button-background"/>
</button>
{props.type === 'pdf' && (
<button
tabIndex={-1}
Expand Down
1 change: 1 addition & 0 deletions src/common/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ export interface Annotation {
pageLabel?: string;
position: Position;
text?: string;
image?: string;
comment?: string;
tags: string[];
dateCreated: string;
Expand Down
116 changes: 113 additions & 3 deletions src/dom/common/components/overlay/annotation-overlay.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,18 @@ import React, {
import {
caretPositionFromPoint,
collapseToOneCharacterAtStart,
findImageInRange,
splitRangeToTextNodes,
supportsCaretPositionFromPoint
} from "../../lib/range";
import { AnnotationType } from "../../../../common/types";
import ReactDOM from "react-dom";
import { IconNoteLarge } from "../../../../common/components/common/icons";
import { closestElement } from "../../lib/nodes";
import { isSafari } from "../../../../common/lib/utilities";
import {
isFirefox,
isSafari
} from "../../../../common/lib/utilities";

export type DisplayedAnnotation = {
id?: string;
Expand Down Expand Up @@ -158,6 +162,31 @@ export const AnnotationOverlay: React.FC<AnnotationOverlayProps> = (props) => {
onDragStart={handleDragStart}
pointerEventsSuppressed={pointerEventsSuppressed}
/>
{annotations.filter(annotation => annotation.type == 'image').map((annotation) => {
if (annotation.id) {
return (
<Image
annotation={annotation}
key={annotation.key}
selected={selectedAnnotationIDs.includes(annotation.id)}
onPointerDown={handlePointerDown}
onPointerUp={handlePointerUp}
onDragStart={handleDragStart}
pointerEventsSuppressed={pointerEventsSuppressed}
/>
);
}
else {
return (
<Image
annotation={annotation}
key={annotation.key}
selected={false}
pointerEventsSuppressed={true}
/>
);
}
})}
</svg>
</>;
};
Expand Down Expand Up @@ -453,7 +482,7 @@ type StaggeredNotesProps = {
};

const SelectionBorder: React.FC<SelectionBorderProps> = React.memo((props) => {
let { rect, preview } = props;
let { rect, preview, strokeWidth = 2 } = props;
return (
<rect
x={rect.left - 5}
Expand All @@ -463,13 +492,14 @@ const SelectionBorder: React.FC<SelectionBorderProps> = React.memo((props) => {
fill="none"
stroke={preview ? '#aaaaaa' : '#6d95e0'}
strokeDasharray="10 6"
strokeWidth={2}/>
strokeWidth={strokeWidth}/>
);
}, (prev, next) => JSON.stringify(prev.rect) === JSON.stringify(next.rect));
SelectionBorder.displayName = 'SelectionBorder';
type SelectionBorderProps = {
rect: DOMRect;
preview?: boolean;
strokeWidth?: number;
};

const RangeSelectionBorder: React.FC<RangeSelectionBorderProps> = (props) => {
Expand Down Expand Up @@ -719,3 +749,83 @@ type CommentIconProps = {
onDragStart?: (event: React.DragEvent) => void;
onDragEnd?: (event: React.DragEvent) => void;
};

const Image: React.FC<ImageProps> = (props) => {
let { annotation, selected, pointerEventsSuppressed, onPointerDown, onPointerUp, onDragStart } = props;
let doc = annotation.range.commonAncestorContainer.ownerDocument;
if (!doc || !doc.defaultView) {
return null;
}

let handleDragStart = (event: React.DragEvent) => {
if (!event.dataTransfer) return;
let image = findImageInRange(annotation.range);
if (image) {
let br = image.getBoundingClientRect();
if (isFirefox) {
// The spec says that if an HTMLImageElement is passed to setDragImage(), the drag image should be the
// element's underlying image data at full width/height. Most browsers choose to ignore the spec and
// draw the image at its displayed width/height, which is actually what we want here. Firefox follows
// the spec, so we have to scale using a canvas.
let canvas = doc!.createElement('canvas');
canvas.width = image.width;
canvas.height = image.height;
let ctx = canvas.getContext('2d')!;
ctx.drawImage(image, 0, 0, image.width, image.height);
event.dataTransfer.setDragImage(canvas, event.clientX - br.left, event.clientY - br.top);
}
else {
event.dataTransfer.setDragImage(image, event.clientX - br.left, event.clientY - br.top);
}
}
onDragStart?.(annotation, event.dataTransfer);
};

let rect = annotation.range.getBoundingClientRect();
rect.x += doc.defaultView.scrollX;
rect.y += doc.defaultView.scrollY;
return <>
{!pointerEventsSuppressed && (
<foreignObject x={rect.x} y={rect.y} width={rect.width} height={rect.height}>
<div
// @ts-ignore
xmlns="http://www.w3.org/1999/xhtml"
style={{
pointerEvents: 'auto',
cursor: 'default',
width: '100%',
height: '100%',
}}
draggable={true}
onPointerDown={onPointerDown ? (event => onPointerDown!(annotation, event)) : undefined}
onPointerUp={onPointerUp ? (event => onPointerUp!(annotation, event)) : undefined}
onDragStart={handleDragStart}
data-annotation-id={props.annotation?.id}
/>
</foreignObject>
)}
{selected
? <SelectionBorder rect={rect} strokeWidth={3}/>
: <rect
x={rect.x - 5}
y={rect.y - 5}
width={rect.width + 10}
height={rect.height + 10}
stroke={annotation.color}
strokeWidth={3}
fill="none"
/>}
{annotation.comment && (
<CommentIcon x={rect.x - 5} y={rect.y - 5} color={annotation.color!}/>
)}
</>;
};

type ImageProps = {
annotation: DisplayedAnnotation;
selected: boolean;
onPointerDown?: (annotation: DisplayedAnnotation, event: React.PointerEvent) => void;
onPointerUp?: (annotation: DisplayedAnnotation, event: React.PointerEvent) => void;
onDragStart?: (annotation: DisplayedAnnotation, dataTransfer: DataTransfer) => void;
pointerEventsSuppressed: boolean;
}
Loading

0 comments on commit 373db57

Please sign in to comment.