Compare commits
6 Commits
refactor
...
normalise-
| Author | SHA1 | Date | |
|---|---|---|---|
| 0871c8595a | |||
| 15028da9ff | |||
| 9a4a0a490e | |||
| c69fc1cbba | |||
| 323dfb72d6 | |||
| 6ddbecee13 |
59
README.md
59
README.md
@@ -1,5 +1,62 @@
|
||||
# IIIF Manifest service for the GreekSchools Project
|
||||
|
||||
This repository holds the code for a NodeJS/Express service that implements dynamic generation of IIIF manifests, compliant with version 2 of the Presentation API. Support for version 3 should be added in the future.
|
||||
This repository holds the code for a NodeJS/Express service that implements dynamic generation of IIIF manifests for images produced by the [GreekSchools ERC project](https://greekschools.eu). The service is compliant with version 2 of the Presentation API, support for version 3 should be added in the future.
|
||||
|
||||
The project uses `yarn` for dependency management and an `.env` file to set environment variables, an example of which can be found in `.env.example`.
|
||||
|
||||
## Installation
|
||||
|
||||
`GreekManifests` requires NodeJS v. >= 20 to be installed on the system, as well as `yarn` as a package manager, which can be installed globally via `npm`:
|
||||
|
||||
```shell
|
||||
npm install -g yarn
|
||||
```
|
||||
|
||||
To install the service itself, clone this repository on the target host (replace `<target_dir>` with a suitable path, or remove to install in `./greek-manifests`):
|
||||
|
||||
```shell
|
||||
git clone https://git.electricmandarine.cloud/nicolo/greek-manifests <target_dir>
|
||||
```
|
||||
|
||||
then run the following commands from the root folder:
|
||||
|
||||
```shell
|
||||
yarn
|
||||
node app.mjs
|
||||
```
|
||||
|
||||
This will start the [Express](https://expressjs.com) web server, which will remain attached to the terminal. This is suitable for testing purposes, for a production instance the Node process should be handled via a `systemd` unit or with [PM2](https://pm2.keymetrics.io), in addition to a reverse proxy like [Nginx](https://nginx.org/en/) or [Caddy](https://caddyserver.com/docs/).
|
||||
|
||||
|
||||
## Documentation
|
||||
|
||||
Automatic [JSDoc](https://jsdoc.app) documentation for the codebase can be generated by running the following command from the project's root folder:
|
||||
|
||||
```shell
|
||||
jsdoc -c jsdoc.json
|
||||
```
|
||||
|
||||
assuming that `jsdoc` is available globally (or locally for the user). The HTML documentation will be available in `docs/`, open `docs/index.html` with a browser to view it.
|
||||
|
||||
Here follows a basic description of the project's strcture and its main APIs.
|
||||
|
||||
### IIIF Resources
|
||||
|
||||
Relevant IIIF Resources are modeled as JavaScript classes in `src/iiif`, with an `IIIFResource` base class that provides a shared constructor. The classes are:
|
||||
|
||||
- `Manifest`: represents a IIIF manifest object
|
||||
- `Canvas`: represents a IIIF canvas object
|
||||
- `Image`: represent an image annotation associated with a canvas
|
||||
- `Sequence`: a list of canvases, required by v2 of the IIIF Presentation API, it will be removed when moving to v3 (see also "[Presentation API support](#presentation-api-support)").
|
||||
|
||||
### Services
|
||||
|
||||
...
|
||||
|
||||
### Routes
|
||||
|
||||
...
|
||||
|
||||
### Presentation API support
|
||||
|
||||
Currently, the service only supports version 2 of the [IIIF Presentation API](https://iiif.io/api/presentation/2.0/), but support for version 3 is planned. It's possible that the manifest and canvas URIs will reflect the version number to keep both v2 and v3 functioning.
|
||||
|
||||
@@ -6,14 +6,15 @@ export const authors = {
|
||||
};
|
||||
|
||||
export const TECH_NAMES = {
|
||||
dn: "Disegni Napoletani",
|
||||
do: "Disegni Oxoniensi",
|
||||
nir: "Near Infrared Imaging 1000nm",
|
||||
visr: "Visible Raking Light",
|
||||
dn: "Neapolitan drawing",
|
||||
do: "Oxonian drawing",
|
||||
nir: "NIR 1000 nm",
|
||||
visr: "Raking-light VIS",
|
||||
hsi: "SWIR Hyperspectral Imaging",
|
||||
uvf: "Technical Photography UVF",
|
||||
mbi: "Multispectral Imaging",
|
||||
hiroxnir: "HIROX Near Infrared",
|
||||
hiroxnir: "HIROX NIR",
|
||||
splitview: "Split view VISr/NIR",
|
||||
};
|
||||
|
||||
export const COPYRIGHT = {
|
||||
|
||||
@@ -1,3 +1,17 @@
|
||||
/**
|
||||
* @typedef MetadataTerms
|
||||
* @property {String} papyrus,
|
||||
* @property {String} author,
|
||||
* @property {String} title,
|
||||
* @property {String} edition,
|
||||
* @property {String} technique,
|
||||
* @property {String} techniqueAcronym,
|
||||
* @property {String} pca,
|
||||
* @property {String} date,
|
||||
* @property {String} imageAuthor,
|
||||
* @property {String} license,
|
||||
* @property {String} copyright
|
||||
*/
|
||||
|
||||
class ManifestMetadata {
|
||||
papyrus = '';
|
||||
@@ -5,6 +19,7 @@ class ManifestMetadata {
|
||||
title = '';
|
||||
edition = '';
|
||||
technique = '';
|
||||
techniqueAcronym = '';
|
||||
pca = null;
|
||||
date = '';
|
||||
imageAuthor = '';
|
||||
@@ -14,18 +29,7 @@ class ManifestMetadata {
|
||||
|
||||
/**
|
||||
* @todo Maybe this doesn't make any sense??
|
||||
* @param {
|
||||
* {papyrus,
|
||||
* author,
|
||||
* title,
|
||||
* edition,
|
||||
* technique,
|
||||
* pca,
|
||||
* date,
|
||||
* imageAuthor,
|
||||
* license,
|
||||
* copyright}
|
||||
* } metadata
|
||||
* @param {MetadataTerms} metadata
|
||||
*/
|
||||
constructor(metadata) {
|
||||
this.papyrus = metadata.papyrus ?? this.papyrus;
|
||||
@@ -35,6 +39,8 @@ class ManifestMetadata {
|
||||
// There should always be a technique value
|
||||
// in the metadata param
|
||||
this.technique = metadata.technique;
|
||||
// Return the key that corresponds to the technique value
|
||||
this.techniqueAcronym = metadata.techniqueAcronym.toUpperCase();
|
||||
this.date = metadata.date ?? this.date;
|
||||
this.imageAuthor = metadata.imageAuthor ?? this.imageAuthor
|
||||
this.license = metadata.license ?? this.license;
|
||||
@@ -52,6 +58,7 @@ class ManifestMetadata {
|
||||
{label:"Title", value: this.title},
|
||||
{label:"Reference edition", value: this.edition},
|
||||
{label:"Technique", value: this.technique},
|
||||
{label:"Technique acronym", value: this.techniqueAcronym},
|
||||
{label:"Date (Year)", value: this.date},
|
||||
{label:"Image Author", value: this.imageAuthor},
|
||||
{label:"License", value: this.license},
|
||||
|
||||
@@ -28,7 +28,7 @@ import { authors, COPYRIGHT } from '../constants.js';
|
||||
* @property {String} subfolder
|
||||
*/
|
||||
|
||||
const namePos = {
|
||||
export const namePos = {
|
||||
hiroxnir: 1,
|
||||
nir: 1,
|
||||
visr: 1,
|
||||
@@ -37,6 +37,28 @@ const namePos = {
|
||||
dn: 2
|
||||
};
|
||||
|
||||
/**
|
||||
* Remove leading zeroes
|
||||
* from canvas and image file
|
||||
* name segments (parts)
|
||||
* @see getCanvasName()
|
||||
* @see ManifestBuilder.getImageName()
|
||||
* @param {String} segment
|
||||
*/
|
||||
export const normaliseSegment = (segment) => {
|
||||
return segment.replaceAll(/^([a-z]+)?0*(\d+)/ig, '$1$2');
|
||||
}
|
||||
/**
|
||||
* Normalize a full canvas name
|
||||
* @param {String} name
|
||||
*/
|
||||
export const normaliseName = (name) => {
|
||||
// Account for separate parts in name
|
||||
// and rejoin them after normalisation
|
||||
// also replace any "slipping" file extension
|
||||
return name.split('&').map(normaliseSegment).join('&').replace(/\.[a-z\d]+$/ig,'');
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {string} imgFilename
|
||||
* @returns {ParsedMetadata}
|
||||
@@ -162,7 +184,7 @@ const extractors = {
|
||||
* @param {string} technique
|
||||
* @returns {ParsedMetadata}
|
||||
*/
|
||||
export function parse (imgFilename, technique) {
|
||||
export function parse(imgFilename, technique) {
|
||||
return extractors[technique](imgFilename);
|
||||
}
|
||||
/**
|
||||
@@ -180,7 +202,7 @@ export function getCanvasName(imgFilename, technique) {
|
||||
canvasName += imgFilename.split('_')[3].replace(/\..*$/,'');
|
||||
}
|
||||
|
||||
return canvasName;
|
||||
return normaliseName(canvasName);
|
||||
}
|
||||
/**
|
||||
* Generate canvas label from canvasName
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
'use strict';
|
||||
|
||||
import * as fs from 'fs';
|
||||
import Manifest from '../iiif/Manifest.js';
|
||||
import { normaliseName, normaliseSegment, namePos } from './FilenameParser.js';
|
||||
/**
|
||||
* Handles filesystem for image repository
|
||||
* @module ImageRepository
|
||||
@@ -58,4 +60,35 @@ export async function getParamsFromFolders () {
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
/**
|
||||
* This extacts a segment to be used
|
||||
* in getImageName(), accounting for the
|
||||
* HSI special case
|
||||
* @param {string} filename The image filename
|
||||
* @param {string} technique
|
||||
* @returns
|
||||
*/
|
||||
function getMatchableSegment(filename, technique) {
|
||||
const parts = filename.split('_');
|
||||
if (technique === 'hsi') {
|
||||
const name = normaliseSegment(parts[1]);
|
||||
const pca = parts[3].replace(/\.[a-z\d]+$/i, '').toLowerCase();
|
||||
return `${name}${pca}`;
|
||||
}
|
||||
return normaliseName(parts[namePos[technique]]);
|
||||
}
|
||||
/**
|
||||
* Get image name for given canvas
|
||||
* @param {string} name The Canvas name
|
||||
* @param {Manifest} manifest The Manifest object
|
||||
* @returns {string}
|
||||
*/
|
||||
export async function getImageName(name, manifest) {
|
||||
const images = await getImageList(manifest.resourceId);
|
||||
let adjustedCanvasName = name;
|
||||
|
||||
return images.filter(i => {
|
||||
return getMatchableSegment(i, manifest.technique) === adjustedCanvasName;
|
||||
})[0];
|
||||
}
|
||||
@@ -6,7 +6,7 @@ import Canvas from '../iiif/Canvas.js';
|
||||
import Image from '../iiif/Image.js';
|
||||
import ManifestMetadata from '../iiif/Metadata.js';
|
||||
import { parse, getCanvasLabel, getCanvasName } from './FilenameParser.js';
|
||||
import { getImageList } from './ImageRepository.js';
|
||||
import { getImageList, getImageName } from './ImageRepository.js';
|
||||
import { TECH_NAMES } from '../constants.js';
|
||||
|
||||
/**
|
||||
@@ -52,7 +52,7 @@ export async function buildManifest(manifestId) {
|
||||
export async function buildCanvas(manifestId, name) {
|
||||
const manifest = new Manifest(IIIF_API_VERSION, BASE_URL);
|
||||
manifest.generateID(manifestId);
|
||||
let filename = await getImageName(name, manifestId);
|
||||
let filename = await getImageName(name, manifest);
|
||||
|
||||
return createCanvas(manifest, filename);
|
||||
}
|
||||
@@ -141,28 +141,6 @@ async function populateCanvases (manifest, images) {
|
||||
|
||||
return manifest;
|
||||
}
|
||||
/**
|
||||
* Get image name for given canvas
|
||||
* @todo Use regex in filter!!
|
||||
* @param {String} name The canvas name
|
||||
* @param {String} manifestId The manifest (resource) id
|
||||
* @returns {string}
|
||||
*/
|
||||
async function getImageName(name, manifestId) {
|
||||
const images = await getImageList(manifestId);
|
||||
|
||||
// Adjust canvas name for HSI with PCA...
|
||||
if (/pc(1|3)/.test(name)) {
|
||||
name = name.replace(
|
||||
/pc((1|3))/,
|
||||
function (match, group1) {
|
||||
return `_HSI_PC${group1}`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
return images.filter(i => i.includes(name))[0];
|
||||
}
|
||||
/**
|
||||
* @param {Manifest} manifest The Manifest object
|
||||
* @param {string} imgFilename
|
||||
@@ -171,6 +149,7 @@ async function getImageName(name, manifestId) {
|
||||
function createMetadata(manifest, imgFilename) {
|
||||
let metadata = parse(imgFilename, manifest.technique);
|
||||
metadata.technique = TECH_NAMES[manifest.technique];
|
||||
metadata.techniqueAcronym = manifest.technique;
|
||||
|
||||
return new ManifestMetadata(metadata);
|
||||
}
|
||||
Reference in New Issue
Block a user