Skip to content

Commit

Permalink
feat(annotation): export publication annotations to w3c annotation js…
Browse files Browse the repository at this point in the history
…onld format

export to the filesystem with "choose a directory" dialog system under
the filename uuid.annotation
the export button is located on the publication card menu bellow export
publication

This a POC feature, for the moment the json file cannot be imported on
any reading system.
  • Loading branch information
panaC committed Jul 1, 2024
1 parent 85ce170 commit 079471f
Show file tree
Hide file tree
Showing 12 changed files with 401 additions and 1 deletion.
29 changes: 29 additions & 0 deletions src/common/redux/actions/annotation/exportW3CAnnotationSet.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// ==LICENSE-BEGIN==
// Copyright 2017 European Digital Reading Lab. All rights reserved.
// Licensed to the Readium Foundation under one or more contributor license agreements.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file exposed on Github (readium) in the project repository.
// ==LICENSE-END==

import { Action } from "readium-desktop/common/models/redux";

export const ID = "ANNOTATION_EXPORT_W3CANNOTATIONSET";

// eslint-disable-next-line @typescript-eslint/no-empty-interface
export interface Payload {
publicationIdentifier: string;
// filePath: string,
}

export function build(publicationIdentifier: string/*, filePath: string*/): Action<typeof ID, Payload> {

return {
type: ID,
payload: {
publicationIdentifier,
// filePath,
},
};
}
build.toString = () => ID; // Redux StringableActionCreator
export type TAction = ReturnType<typeof build>;
12 changes: 12 additions & 0 deletions src/common/redux/actions/annotation/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
// ==LICENSE-BEGIN==
// Copyright 2017 European Digital Reading Lab. All rights reserved.
// Licensed to the Readium Foundation under one or more contributor license agreements.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file exposed on Github (readium) in the project repository.
// ==LICENSE-END==

import * as exportW3CAnnotationSetFromAnnotations from "./exportW3CAnnotationSet";

export {
exportW3CAnnotationSetFromAnnotations,
};
2 changes: 2 additions & 0 deletions src/common/redux/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import * as publicationActions from "./publication";
import * as themeActions from "./theme";
import * as wizardActions from "./wizard";
import * as versionUpdateActions from "./version-update";
import * as annotationActions from "./annotation";

export {
historyActions,
Expand All @@ -45,4 +46,5 @@ export {
themeActions,
wizardActions,
versionUpdateActions,
annotationActions,
};
88 changes: 88 additions & 0 deletions src/main/redux/sagas/annotation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
// ==LICENSE-BEGIN==
// Copyright 2017 European Digital Reading Lab. All rights reserved.
// Licensed to the Readium Foundation under one or more contributor license agreements.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file exposed on Github (readium) in the project repository.
// ==LICENSE-END==

import * as debug_ from "debug";
// import { LocaleConfigIdentifier, LocaleConfigValueType } from "readium-desktop/common/config";
import { annotationActions } from "readium-desktop/common/redux/actions";
import { takeSpawnLeading } from "readium-desktop/common/redux/sagas/takeSpawnLeading";
import { error } from "readium-desktop/main/tools/error";
// eslint-disable-next-line local-rules/typed-redux-saga-use-typed-effects
import { call as callTyped, select as selectTyped } from "typed-redux-saga/macro";
import { getPublication } from "./api/publication/getPublication";
import { convertAnnotationListToW3CAnnotationModelSet, convertPublicationToAnnotationStateAbout } from "readium-desktop/main/w3c/annotation/converter";
import { RootState } from "../states";
import { saveW3CAnnotationModelSetFromFilePath } from "readium-desktop/main/w3c/annotation/fs";
import { getLibraryWindowFromDi } from "readium-desktop/main/di";
import { dialog } from "electron";
import { promises as fsp } from "fs";
import * as path from "path";
import { IAnnotationState } from "readium-desktop/common/redux/states/renderer/annotation";

// Logger
const filename_ = "readium-desktop:main:saga:annotation";
const debug = debug_(filename_);
debug("_");

function* exportW3CAnnotationWithFilePath(publicationIdentifier: string, filePath: string) {

const pub = yield* callTyped(getPublication, publicationIdentifier);

let annotations: IAnnotationState[] = [];

const sessionReader = yield* selectTyped((state: RootState) => state.win.session.reader);
const winSessionReaderState = Object.values(sessionReader).find((v) => v.publicationIdentifier === publicationIdentifier);
if (winSessionReaderState) {
annotations = (winSessionReaderState?.reduxState?.annotation || []).map(([,v]) => v);
} else {
const sessionRegistry = yield* selectTyped((state: RootState) => state.win.registry.reader);
if (Object.keys(sessionRegistry).find((v) => v === publicationIdentifier)) {
annotations = (sessionRegistry[publicationIdentifier]?.reduxState?.annotation || []).map(([, v]) => v);
}
}

const publicationMetadata = yield* callTyped(convertPublicationToAnnotationStateAbout, pub, publicationIdentifier);
const W3CAnnotationSet = yield* callTyped(convertAnnotationListToW3CAnnotationModelSet, annotations, publicationMetadata);

yield* callTyped(saveW3CAnnotationModelSetFromFilePath, filePath, W3CAnnotationSet);
}

export function* exportW3CAnnotation(action: annotationActions.exportW3CAnnotationSetFromAnnotations.TAction) {

const { publicationIdentifier } = action.payload;

const libraryAppWindow = yield* callTyped(() => getLibraryWindowFromDi());

// Open a dialog to select a folder then copy the publication in it
const res = yield* callTyped(() => dialog.showOpenDialog(
libraryAppWindow ? libraryAppWindow : undefined,
{
properties: ["openDirectory"],
},
));

if (!res.canceled) {
if (res.filePaths && res.filePaths.length > 0) {
let destinationPath = res.filePaths[0];
// If the selected path is a file then choose the directory containing this file
const stat = yield* callTyped(() => fsp.stat(destinationPath));
if (stat?.isFile()) {
destinationPath = path.join(path.dirname(destinationPath));
}
destinationPath = path.join(destinationPath, `${publicationIdentifier}.annotation`);
yield* callTyped(exportW3CAnnotationWithFilePath, publicationIdentifier, destinationPath);
}
}
}

export function saga() {

return takeSpawnLeading(
annotationActions.exportW3CAnnotationSetFromAnnotations.ID,
exportW3CAnnotation,
(e) => error(filename_, e),
);
}
4 changes: 4 additions & 0 deletions src/main/redux/sagas/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import * as win from "./win";
import * as telemetry from "./telemetry";
import * as lcp from "./lcp";
import * as catalog from "./catalog";
import * as annotation from "./annotation";

import { IS_DEV } from "readium-desktop/preprocessor-directives";
// Logger
Expand Down Expand Up @@ -124,6 +125,9 @@ export function* rootSaga() {
// need to track the previous state version before update in initSuccess.build
yield call(telemetry.collectSaveAndSend);

// export annotations
yield annotation.saga();

// app initialized
yield put(appActions.initSuccess.build());

Expand Down
98 changes: 98 additions & 0 deletions src/main/w3c/annotation/annotationModel.type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// ==LICENSE-BEGIN==
// Copyright 2017 European Digital Reading Lab. All rights reserved.
// Licensed to the Readium Foundation under one or more contributor license agreements.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file exposed on Github (readium) in the project repository.
// ==LICENSE-END==

export interface IW3CAnnotationModel {
"@context": string;
id: string;
created: string;
modified: string;
type: string;
hash: string;
body: {
type: string;
value: string;
format: string;
color: string;
textDirection?: string;
language?: string;
};
target: {
source: string;
meta: {
headings: {
level: number;
txt: string;
}[];
page: string;
};
selector: (
| {
type: "TextQuoteSelector";
exact: string;
prefix: string;
suffix: string;
}
| {
type: "ProgressionSelector";
value: number;
}
| {
type: "DomRangeSelector";
startContainerElementCssSelector: string;
startContainerChildTextNodeIndex: number;
startOffset: number;
endContainerElementCssSelector: string;
endContainerChildTextNodeIndex: number;
endOffset: number;
}
| {
type: "FragmentSelector";
conformsTo: string;
value: string;
}
)[];
};
}

interface Generator {
id: string;
type: string;
name: string;
homepage: string;
}

export interface IW3CAnnotationSetAboutView {
identiferArrayString: string[];
mimeType: string;
title: string;
publisher: string[];
creator: string[];
publishedAt: string;
source: string;
}

interface About {
"dc:identifier": string[];
"dc:format": string;
"dc:title": string;
"dc:publisher": string[];
"dc:creator": string[];
"dc:date": string;
"dc:source"?: string;
}

export interface IW3CAnnotationModelSet {
"@context": string;
id: string;
type: string;
generator: Generator;
generated: string;
label: string;
about: About;
total: number,
items: IW3CAnnotationModel[];
}
127 changes: 127 additions & 0 deletions src/main/w3c/annotation/converter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
// ==LICENSE-BEGIN==
// Copyright 2017 European Digital Reading Lab. All rights reserved.
// Licensed to the Readium Foundation under one or more contributor license agreements.
// Use of this source code is governed by a BSD-style license
// that can be found in the LICENSE file exposed on Github (readium) in the project repository.
// ==LICENSE-END==

import { IW3CAnnotationModel, IW3CAnnotationModelSet, IW3CAnnotationSetAboutView } from "./annotationModel.type";
import { v4 as uuidv4 } from "uuid";
import { _APP_NAME, _APP_VERSION } from "readium-desktop/preprocessor-directives";
import { PublicationView } from "readium-desktop/common/views/publication";
import { IAnnotationState } from "readium-desktop/common/redux/states/renderer/annotation";

function rgbToHex(color: { red: number; green: number; blue: number }): string {
const { red, green, blue } = color;
const redHex = Math.min(255, Math.max(0, red)).toString(16).padStart(2, "0");
const greenHex = Math.min(255, Math.max(0, green)).toString(16).padStart(2, "0");
const blueHex = Math.min(255, Math.max(0, blue)).toString(16).padStart(2, "0");
return `#${redHex}${greenHex}${blueHex}`;
}

export function convertAnnotationToW3CAnnotationModel(annotation: IAnnotationState): IW3CAnnotationModel {

const currentDate = new Date();
const dateString: string = currentDate.toISOString();
const { uuid, color, locatorExtended: def } = annotation;
const { selectionInfo, locator, headings, epubPage } = def;
const { cleanText, rawText, rawBefore, rawAfter } = selectionInfo;
const { href } = locator;

return {
"@context": "http://www.w3.org/ns/anno.jsonld",
id: uuid ? "urn:uuid:" + uuid : "",
created: dateString,
modified: dateString,
type: "Annotation",
hash: "",
body: {
type: "TextualBody",
value: cleanText || "",
format: "text/plain",
color: rgbToHex(color),
// textDirection: "ltr",
// language: "fr",
},
target: {
source: href || "",
meta: {
headings: (headings || []).map(({ txt, level }) => ({ level, txt })),
page: epubPage || "",
},
selector: [
{
type: "TextQuoteSelector",
exact: rawText || "",
prefix: rawBefore || "",
suffix: rawAfter || "",
},
{
type: "ProgressionSelector",
value: locator.locations?.progression || 0,
},
{
type: "DomRangeSelector",
startContainerElementCssSelector: selectionInfo?.rangeInfo?.startContainerElementCssSelector || "",
startContainerChildTextNodeIndex: selectionInfo?.rangeInfo?.startContainerChildTextNodeIndex || 0,
startOffset: selectionInfo?.rangeInfo?.startOffset || 0,
endContainerElementCssSelector: selectionInfo?.rangeInfo?.endContainerElementCssSelector || "",
endContainerChildTextNodeIndex: selectionInfo?.rangeInfo?.endContainerChildTextNodeIndex || 0,
endOffset: selectionInfo?.rangeInfo?.endOffset || 0,
},
{
type: "FragmentSelector",
conformsTo: "http://www.idpf.org/epub/linking/cfi/epub-cfi.html",
value: `epubcfi(${selectionInfo?.rangeInfo?.cfi || locator.locations?.cfi || ""})`,
},
],
},
};
}

export function convertAnnotationListToW3CAnnotationModelSet(annotationArray: IAnnotationState[],
publicationMetadata: IW3CAnnotationSetAboutView,
): IW3CAnnotationModelSet {

const { identiferArrayString, mimeType, title, publisher, creator, publishedAt, source } = publicationMetadata;
const currentDate = new Date();
const dateString: string = currentDate.toISOString();

return {
"@context": "http://www.w3.org/ns/anno.jsonld",
id: "urn:uuid:" + uuidv4(),
type: "AnnotationSet",
generator: {
id: "https://github.com/edrlab/thorium-reader/releases/tag/v" + _APP_VERSION,
type: "Software",
name: _APP_NAME + " " + _APP_VERSION,
homepage: "https://thorium.edrlab.org",
},
generated: dateString,
label: "Annotations set",
about: {
"dc:identifier": identiferArrayString || [],
"dc:format": mimeType || "",
"dc:title": title || "",
"dc:publisher": publisher || [],
"dc:creator": creator || [],
"dc:date": publishedAt || "",
"dc:source": source || "",
},
total: annotationArray.length,
items: (annotationArray || []).map((v) => convertAnnotationToW3CAnnotationModel(v)),
};
}

export function convertPublicationToAnnotationStateAbout(publicationView: PublicationView, publicationIdentifier: string): IW3CAnnotationSetAboutView {

return {
identiferArrayString: ["urn:isbn:" + publicationView.workIdentifier || ""],
mimeType: "application/epub+zip",
title: publicationView.documentTitle || "",
publisher: publicationView.publishers || [],
creator: publicationView.authors || [],
publishedAt: publicationView.publishedAt || "",
source: "urn:uuid:" + publicationIdentifier,
};
}
Loading

0 comments on commit 079471f

Please sign in to comment.