Substantial refactoring of code structure

This commit is contained in:
2026-03-23 15:20:30 +01:00
parent 4b9b5b737d
commit 012ad62382
23 changed files with 565 additions and 482 deletions

View File

@@ -1,3 +1,4 @@
DEBUG='dev|prod'
PORT = 8080 PORT = 8080
BASE_URL = 'https://something.com' BASE_URL = 'https://something.com'
# The most recent API version number that should be supported # The most recent API version number that should be supported

5
README.md Normal file
View File

@@ -0,0 +1,5 @@
# 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.
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`.

13
app.mjs
View File

@@ -1,12 +1,10 @@
import 'dotenv/config';
import createError from 'http-errors'; import createError from 'http-errors';
import logger from 'morgan'; import logger from 'morgan';
import cors from 'cors';
import express from 'express'; import express from 'express';
import path from 'path';
import * as dotenv from 'dotenv';
import router from './routes/index.mjs'; import router from './routes/index.mjs';
dotenv.config();
let indexRouter = router; let indexRouter = router;
let app = express(); let app = express();
@@ -17,10 +15,13 @@ const PORT = process.env.PORT || 3000;
//app.set('views', path.join(__dirname, 'views')); //app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
app.use(logger('dev')); // Equivalent to Access-Control-Allow-Origin: *
// if not configured otherwise
app.use(cors());
app.use(logger(process.env.DEBUG));
app.use(express.json()); app.use(express.json());
app.use(express.urlencoded({ extended: false })); app.use(express.urlencoded({ extended: false }));
//app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter); app.use('/', indexRouter);

View File

@@ -1,7 +1,5 @@
import Canvas from '../src/Canvas.js'; import { buildCanvas } from '../src/service/ManifestBuilder.js';
import Image from '../src/Image.js';
import Manifest from '../src/Manifest.js';
import Common from '../src/common.js';
/** /**
* Generate a canvas object to serve * Generate a canvas object to serve
* @todo Use createCanvas from Common? * @todo Use createCanvas from Common?
@@ -10,47 +8,5 @@ import Common from '../src/common.js';
* @param {number|string} name The canvas name * @param {number|string} name The canvas name
*/ */
export default async function generateCanvas(manifestId, name) { export default async function generateCanvas(manifestId, name) {
const IIIF_API_VERSION = process.env.IIIF_API_VERSION; return buildCanvas(manifestId, name);
const BASE_URL = process.env.BASE_URL;
const canvas = new Canvas(IIIF_API_VERSION, BASE_URL);
const manifest = new Manifest(IIIF_API_VERSION, BASE_URL);
manifest.generateID(manifestId);
canvas.generateID(manifestId, name);
let filename = await Common.getImageName(canvas)
console.log('Filename: ' + filename);
let label = name.replace(
/c(\w{1,2})0+(\d+).*(\.\w{2,3})?$/i,
function (str, c, number) {
return `C${c}. ${number}`;
});
// Add PCA to canvas label for HSI images
if (manifest.technique === 'hsi') {
label += ` ${filename.split('_')[3].replace(/\..*$/,'')}`;
}
canvas.label = label;
const image = new Image(canvas.id, filename);
image.generateID(process.env.IMAGE_SERVER_URL, filename);
const imgSize = await Common.getImageSize(image.id);
image.setSize(imgSize.height, imgSize.width);
canvas.setThumbnail(
imgSize.thumb.height,
imgSize.thumb.width,
image.id
);
canvas.addImage(image);
return canvas.toObject();
/**
const manifest = new Manifest(IIIF_API_VERSION, BASE_URL);
manifest.generateID(manifestId);
return Common.createCanvas(manifest, name, manifestId).toObject();
*/
} }

View File

@@ -1,34 +1,12 @@
'use strict'; 'use strict';
import Manifest from '../src/Manifest.js'; //import Common from '../src/common.js';
import Common from '../src/common.js'; import { buildManifest } from '../src/service/ManifestBuilder.js';
/** /**
* Generate a manifest object to serve * Generate a manifest object to serve
* @param {string} manifestId * @param {string} manifestId
*/ */
export default async function generateManifest(manifestId) { export default async function generateManifest(manifestId) {
const IIIF_API_VERSION = process.env.IIIF_API_VERSION; return buildManifest(manifestId);
const BASE_URL = process.env.BASE_URL;
let manifest = new Manifest(IIIF_API_VERSION, BASE_URL);
manifest.generateID(manifestId);
manifest.generateLabel();
const images = await Common.getImageList(manifestId);
manifest = await Common.populateCanvases(
manifest,
images,
manifestId
);
manifest.setMetadata(
Common.createMetadata(
manifest,
images[0],
)
);
return manifest.toObject();
} }

View File

@@ -1,22 +1,20 @@
'use strict'; 'use strict';
import Common from '../src/common.js'; import { TECH_NAMES } from "../src/constants.js";
import { getParamsFromFolders } from "../src/service/ImageRepository.js";
/** /**
* Show all possible parameters for manifest URLs * Show all possible parameters for manifest URLs
*/ */
export default async function exposeParams() { export default async function exposeParams() {
console.log(process.env.IMAGES_DIR); console.log(process.env.IMAGES_DIR);
let techs = [];
for (let key in Common.TECH_NAMES) { const techs = Object.entries(TECH_NAMES).map(([key, fullname]) => ({
techs.push({ acronym: key.toUpperCase(),
'acronym': key.toUpperCase(), fullname,
'fullname': Common.TECH_NAMES[key] }));
});
}
let papyri = await Common.getParamsFromFolders(); let papyri = await getParamsFromFolders();
return { return {
'techniques' : techs, 'techniques' : techs,

View File

@@ -4,6 +4,10 @@
}, },
"opts": { "opts": {
"plugins": ["plugins/markdown"],
"markdown": {
"tags": ["module", "namespace"]
},
"encoding": "utf8", "encoding": "utf8",
"destination": "docs/", "destination": "docs/",
"recurse": true, "recurse": true,

View File

@@ -1,6 +1,7 @@
{ {
"dependencies": { "dependencies": {
"clean-jsdoc-theme": "^4.2.13", "clean-jsdoc-theme": "^4.2.13",
"cors": "^2.8.6",
"dotenv": "^16.3.1", "dotenv": "^16.3.1",
"ejs": "^3.1.9", "ejs": "^3.1.9",
"express": "^4.18.2", "express": "^4.18.2",

View File

@@ -11,15 +11,18 @@ let router = express.Router();
/* GET manifest JSON */ /* GET manifest JSON */
router.get('/iiif/:manifestid/manifest', async function(req, res) { router.get('/iiif/:manifestid/manifest', async function(req, res) {
let manifest = {}; let manifest = {};
res.set('Access-Control-Allow-Origin', '*');
try { try {
manifest = await generateManifest(req.params.manifestid) manifest = await generateManifest(req.params.manifestid)
} catch(error) { } catch(error) {
console.debug(error);
res.status(500).json({ res.status(500).json({
status: 500, status: 500,
message: 'There was an error processing this request', message: 'There was an error processing this request',
code: error.code ?? 'not available', code: error.code ?? 'not available',
}); });
return;
} }
res.json(manifest); res.json(manifest);
}); });
@@ -27,7 +30,6 @@ router.get('/iiif/:manifestid/manifest', async function(req, res) {
/* GET canvas JSON */ /* GET canvas JSON */
router.get('/iiif/:manifestid/canvas/:name', async function(req, res) { router.get('/iiif/:manifestid/canvas/:name', async function(req, res) {
let canvas = {}; let canvas = {};
res.set('Access-Control-Allow-Origin', '*');
try { try {
canvas = await generateCanvas(req.params.manifestid, req.params.name) canvas = await generateCanvas(req.params.manifestid, req.params.name)
} catch(error) { } catch(error) {
@@ -44,7 +46,6 @@ router.get('/iiif/:manifestid/canvas/:name', async function(req, res) {
/* GET sequence JSON */ /* GET sequence JSON */
router.get('/iiif/:manifestid/sequence/:name', async function(req, res) { router.get('/iiif/:manifestid/sequence/:name', async function(req, res) {
let sequence = {}; let sequence = {};
res.set('Access-Control-Allow-Origin', '*');
try { try {
sequence = await generateSequence(req.params.manifestid, req.params.name) sequence = await generateSequence(req.params.manifestid, req.params.name)
} catch(error) { } catch(error) {
@@ -53,13 +54,13 @@ router.get('/iiif/:manifestid/sequence/:name', async function(req, res) {
message: 'There was an error processing this request', message: 'There was an error processing this request',
code: error.code ?? 'not available', code: error.code ?? 'not available',
}); });
return;
} }
res.json(sequence); res.json(sequence);
}); });
/* GET possible params for manifest responses */ /* GET possible params for manifest responses */
router.get('/params', async function(req, res) { router.get('/params', async function(req, res) {
res.set('Access-Control-Allow-Origin', '*');
try { try {
res.json(await exposeParams()); res.json(await exposeParams());
} catch(error) { } catch(error) {

View File

@@ -1,12 +0,0 @@
/**
* @interface
*/
class IIIFResource {
id;
type;
generateId(serviceURL, filename) {}
toObject() {}
}
export default IIIFResource;

View File

@@ -1,292 +0,0 @@
'use strict';
import * as fs from 'fs';
import Manifest from './Manifest.js';
import Sequence from './Sequence.js';
import Canvas from './Canvas.js';
import Image from './Image.js';
import ManifestMetadata from './Metadata.js';
/**
* @namespace Common
*/
const Common = {};
const authors = {
DAN: 'Danilo P. Pavone',
AUR: 'Aurélie Tournié',
SAB: 'Sabrina Samelo',
SOF: 'Sofia Ceccarelli',
};
/**
* @param {string} imgFilename
* @returns {{papyrus:string,imageAuthor:string,date:string}}
*/
function extractHIROXNIRMetadata(imgFilename) {
return {
papyrus: imgFilename.split('_')[0].split('-')[1],
imageAuthor: authors[imgFilename.split('-')[0].replace(/\d{4}/,'')],
date: imgFilename.split('-')[0].match(/\d{4}/)[0],
}
}
/**
* @param {string} imgFilename
* @returns {{papyrus:string,imageAuthor:string,date:string}}
*/
function extractNIRMetadata(imgFilename) {
return {
papyrus: imgFilename.split('_')[0].split('-')[2],
imageAuthor: authors[imgFilename.split('-')[0].replace(/\d{4}/,'')],
date: imgFilename.split('-')[0].match(/\d{4}/)[0],
}
}
/**
* @param {string} imgFilename
* @returns {{papyrus:string,imageAuthor:string,date:string,copyright:string}}
*/
function extractDNMetadata(imgFilename) {
return {
papyrus: imgFilename.split('_')[1],
imageAuthor: '',
date: '',
copyright: "Ministero della Cultura (Biblioteca Nazionale 'Vittorio Emanuele III' di Napoli)",
}
}
/**
* @param {string} imgFilename
* @returns {{papyrus:string,imageAuthor:string,date:string,copyright:string}}
*/
function extractDOMetadata(imgFilename) {
return {
papyrus: imgFilename.split('_')[1],
imageAuthor: '',
date: '',
copyright: "The Bodleian Libraries, University of Oxford",
}
}
Common.TECH_NAMES = {
dn: "Disegni Napoletani",
do: "Disegni Oxoniensi",
nir: "Near Infrared Imaging 1000nm",
hsi: "SWIR Hyperspectral Imaging",
uvf: "Technical Photography UVF",
mbi: "Multispectral Imaging",
hiroxnir: "HIROX Near Infrared",
};
/**
* Retrieves available image techniques
* for all papyruses based on folder contents
* @returns {object}
*/
Common.getParamsFromFolders = async function() {
let params = [];
const papyri = await fs.promises.readdir(process.env.IMAGES_DIR);
for (let p of papyri) {
let techniques = [];
for (let tech of await fs.promises.readdir(`${process.env.IMAGES_DIR}/${p}`)) {
let files = await fs.promises.readdir(
`${process.env.IMAGES_DIR}/${p}/${tech}`
);
files = files.filter(file => /(tiff?|jpe?g|jp2|bmp)/.test(file));
if (files.length) {
techniques.push(tech.replace(/PHerc_\d+_/i, ''));
}
}
params.push({
name : p.replace('_', ' '),
techniques
});
}
return params;
}
/**
* @param {string} manifestId
* @returns {string[]}
* @throws `readdir` will thrown an ENOENT error if the images folder doesn't exist.
* The manifest route should catch it.
*/
Common.getImageList = async function (manifestId) {
// Regex to exclude images with certain patterns in filename
const regexFilter = new RegExp(/(c2r|copertin.|camice|tit)/, 'i');
let folderName = manifestId.replace(/pherc-(\d+)-(\w+)$/, function (_match, g1, g2) {
return `PHerc_${g1}_${g2.toUpperCase()}`;
});
let baseFolder = `${folderName.split('_')[0]}_${folderName.split('_')[1]}`;
let files = await fs.promises.readdir(
`${process.env.IMAGES_DIR}/${baseFolder}/${folderName}`
);
files = files.filter(file => !regexFilter.test(file) && !file.startsWith('.'));
return files;
}
/**
* @param {string} imageId The image's id as a URL to the image server
* @returns {{width: number, height: number, thumb: {width: number, height: number}}}
*/
Common.getImageSize = async function (imageId) {
let infoURL = imageId.replace(/full.*$/,'info.json');
const res = await fetch(infoURL);
let size = {};
if (res.ok) {
const infoJson = await res.json();
const maxSize = infoJson.sizes[infoJson.sizes.length - 1];
size.height = maxSize.height;
size.width = maxSize.width;
size.thumb = {
width: infoJson.sizes[1].width,
height: infoJson.sizes[1].height,
}
}
return size;
}
/**
* Get image name for given canvas
* @todo Use regex in filter!!
* @param {Canvas} canvas
* @returns {string}
*/
Common.getImageName = async function (canvas) {
const images = await this.getImageList(canvas.resourceId);
let name = canvas.name;
// 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}`;
}
);
}
console.log('Canvas name: ' + name);
return images.filter(i => i.includes(name))[0];
}
/**
* Create a canvas from an image filename
* @param {Manifest} manifest
* @param {string} filename The image filename
* @returns {Canvas}
*/
Common.createCanvas = async function (manifest, filename) {
let canvas = new Canvas(
process.env.IIIF_API_VERSION,
process.env.BASE_URL
);
const namePos = {
hiroxnir: 1,
nir: 1,
hsi: 1,
do: 2,
dn: 2
};
// Remove file extension
let canvasName = filename.split('_')[namePos[manifest.technique]]
.replace(/\.\w{1,3}$/, '');
let label = canvasName.replace(
/c(\w{1,2})0+(\d+).*(\.\w{2,3})?$/i,
function (str, c, number) {
return `C${c}. ${number}`;
});
// Add PCA to canvas label for HSI images
if (manifest.technique === 'hsi') {
label += ` ${filename.split('_')[3].replace(/\..*$/,'')}`;
canvasName += `${filename.split('_')[3].replace(/\..*$/,'')}`;
}
canvas.generateID(manifest.resourceId, canvasName.toLowerCase());
canvas.label = label;
let image = new Image(canvas.id);
image.generateID(process.env.IMAGE_SERVER_URL, filename);
const imgSize = await this.getImageSize(image.id);
image.setSize(imgSize.height, imgSize.width);
canvas.setThumbnail(
imgSize.thumb.height,
imgSize.thumb.width,
image.id
);
canvas.addImage(image);
return canvas;
}
/**
* @param {Manifest} manifest The manifest object
* @param {string[]} images List of image filenames from folder
* @returns {Manifest}
*/
Common.populateCanvases = async function (manifest, images) {
const sequence = new Sequence(process.env.BASE_URL);
// There's only one sequence
sequence.generateID(manifest.resourceId, 0);
for (let img of images) {
// Skip failing images (TODO log error to file)
try {
let canvas = await this.createCanvas(manifest, img);
sequence.addCanvas(canvas);
} catch (error) {
console.error(error);
console.log(`\nAffected image: ${img}`);
continue;
}
}
// Sort them according to their ID number... (cornice, colonna, ecc...)
sequence.canvases.sort((a, b) => {
const firstId = a['@id'].slice(a['@id'].lastIndexOf('/') + 1).replace(/[a-z]+/ig,'');
const secondId = b['@id'].slice(b['@id'].lastIndexOf('/') + 1).replace(/[a-z]+/ig,'');
return Number(firstId) - Number(secondId);
});
manifest.addSequence(sequence);
return manifest;
}
/**
* @param {Manifest} manifest The Manifest object
* @param {string} imgFilename
* @returns {ManifestMetadata}
*/
Common.createMetadata = function (manifest, imgFilename) {
let metadata = this.getMetadataFromImgName(imgFilename, manifest.technique);
metadata.technique = Common.TECH_NAMES[manifest.technique];
return new ManifestMetadata(metadata);
}
/**
* @param {string} imgFilename
* @param {string} technique
* @returns {{papyrus:string,imageAuthor:string,date:string,copyright:?string}}
*/
Common.getMetadataFromImgName = function (imgFilename, technique) {
const extractor = {
nir: extractNIRMetadata,
dn: extractDNMetadata,
do: extractDOMetadata,
hsi: extractNIRMetadata,
hiroxnir: extractHIROXNIRMetadata,
}
return extractor[technique](imgFilename);
}
export default Common;

24
src/constants.js Normal file
View File

@@ -0,0 +1,24 @@
export const authors = {
DAN: 'Danilo P. Pavone',
AUR: 'Aurélie Tournié',
SAB: 'Sabrina Samelo',
SOF: 'Sofia Ceccarelli',
};
export const TECH_NAMES = {
dn: "Disegni Napoletani",
do: "Disegni Oxoniensi",
nir: "Near Infrared Imaging 1000nm",
visr: "Visible Raking Light",
hsi: "SWIR Hyperspectral Imaging",
uvf: "Technical Photography UVF",
mbi: "Multispectral Imaging",
hiroxnir: "HIROX Near Infrared",
};
export const COPYRIGHT = {
dn: "Ministero della Cultura (Biblioteca Nazionale 'Vittorio Emanuele III' di Napoli)",
do: "The Bodleian Libraries, University of Oxford",
nir: "CNR - Consiglio Nazionale delle Ricerche, Ministero della Cultura (Biblioteca Nazionale 'Vittorio Emanuele III' di Napoli)",
visr: "CNR - Consiglio Nazionale delle Ricerche, Ministero della Cultura (Biblioteca Nazionale 'Vittorio Emanuele III' di Napoli)",
};

View File

@@ -1,27 +1,30 @@
import IIIFResource from './IIIFResource.js'; import IIIFResource from './IIIFResource.js';
import Image from './Image.js'; import Image from './Image.js';
/** import ManifestMetadata from './Metadata.js';
* @implements IIIFResource
*/ class Canvas extends IIIFResource {
class Canvas {
id = ''; id = '';
#type = 'sc:Canvas'; #type = 'sc:Canvas';
#label = ''; #label = '';
#metadata = {};
resourceId = ''; resourceId = '';
name = ''; name = '';
images = []; images = [];
thumbnail = {}; thumbnail = {};
/**
* @param {Number} IIIFApiVersion
* @param {String} baseURL
*/
constructor(IIIFApiVersion, baseURL) { constructor(IIIFApiVersion, baseURL) {
this.context = `https://iiif.io/api/presentation/${IIIFApiVersion}/context.json`; super(IIIFApiVersion, baseURL);
this.BASE_URL = baseURL;
} }
/** /**
* @param {string} resourceId The resource ID for this canvas * @param {string} resourceId The resource ID for this canvas
* @param {int|string} name A unique name for this canvas * @param {int|string} name A unique name for this canvas
*/ */
generateID(resourceId, name) { generateID(resourceId, name) {
name = name.replace(/cr([1-9]$)/, `cr0$1`); //name = name.replace(/cr([1-9]$)/, `cr0$1`);
this.id = `${this.BASE_URL}/${resourceId}/canvas/${name}`; this.id = `${this.BASE_URL}/${resourceId}/canvas/${name}`;
this.resourceId = resourceId; this.resourceId = resourceId;
this.name = name; this.name = name;
@@ -53,6 +56,12 @@ class Canvas {
width width
} }
} }
/**
* @param {ManifestMetadata} metadata
*/
setMetadata(metadata) {
this.#metadata = metadata.toObject();
}
/** /**
* Object representation * Object representation
* @returns {object} * @returns {object}
@@ -63,6 +72,7 @@ class Canvas {
"@id" : this.id, "@id" : this.id,
"@type" : this.#type, "@type" : this.#type,
"label" : this.#label, "label" : this.#label,
"metadata" : this.#metadata,
"images" : this.images, "images" : this.images,
"thumbnail" : this.thumbnail "thumbnail" : this.thumbnail
} }

19
src/iiif/IIIFResource.js Normal file
View File

@@ -0,0 +1,19 @@
/**
* @interface
*/
class IIIFResource {
/**
* @param {Number} IIIFApiVersion
* @param {String} baseURL
*/
constructor(IIIFApiVersion, baseURL) {
this.context = `https://iiif.io/api/presentation/${IIIFApiVersion}/context.json`;
this.BASE_URL = baseURL;
}
generateId(serviceURL, filename) {}
toObject() {}
}
export default IIIFResource;

View File

@@ -1,53 +1,8 @@
'use strict;'
import { getImagePath } from '../service/FilenameParser.js';
import IIIFResource from './IIIFResource.js'; import IIIFResource from './IIIFResource.js';
/**
* @todo Move to common.js?!
*/
const splitter = {
HIROXNIR: splitHIROXNIR,
NIR: splitNIR,
DN: splitDNO,
DO: splitDNO,
HSI: splitHSI
};
function splitHIROXNIR(filename) {
let splitFilename = filename.split('_');
const papyrusNum = splitFilename[0].split('-')[1];
const baseFolder = `PHerc_${papyrusNum}`;
const subfolder = `PHerc_${papyrusNum}_HIROXNIR`;
return {baseFolder, subfolder};
}
function splitNIR(filename) {
let splitFilename = filename.split('_');
const papyrusNum = splitFilename[0].split('-')[2];
const baseFolder = `PHerc_${papyrusNum}`;
const subfolder = `PHerc_${papyrusNum}_${splitFilename[2].split('-')[0]}`;
return {baseFolder, subfolder};
}
/**
* @todo Redundant...
*/
function splitHSI(filename) {
let splitFilename = filename.split('_');
const papyrusNumb = splitFilename[0].split('-')[2];
const baseFolder = `PHerc_${papyrusNumb}`;
const subfolder = `PHerc_${papyrusNumb}_${splitFilename[2]}`;
return {baseFolder, subfolder};
}
function splitDNO(filename) {
let splitFilename = filename.split('_');
const papyrusNumb = splitFilename[1];
const baseFolder = `PHerc_${papyrusNumb}`;
const subfolder = `PHerc_${papyrusNumb}_${splitFilename[0]}`;
return {baseFolder, subfolder};
}
/** /**
* @implements IIIFResource * @implements IIIFResource
*/ */
@@ -87,11 +42,10 @@ class Image {
* server endpoint for this image * server endpoint for this image
* @param {string} serviceURL The image server base URL * @param {string} serviceURL The image server base URL
* @param {string} filename The image's complete filename * @param {string} filename The image's complete filename
* @param {string} technique The imaging technique (from the manifest)
*/ */
generateID(serviceURL, filename) { generateID(serviceURL, filename, technique) {
let splitFn = splitter[/((HIROX)?NIR|DO|DN|HSI)/.exec(filename)[0]]; const {baseFolder, subfolder} = getImagePath(filename, technique);
const {baseFolder, subfolder} = splitFn(filename);
this.id = `${serviceURL}/${this.#IIIF_API_VERSION}/${baseFolder}%2F${subfolder}%2F${filename}/full/max/0/default.jpg`; this.id = `${serviceURL}/${this.#IIIF_API_VERSION}/${baseFolder}%2F${subfolder}%2F${filename}/full/max/0/default.jpg`;
this.service['@id'] = this.id.replace(/\/full.*$/,''); this.service['@id'] = this.id.replace(/\/full.*$/,'');

View File

@@ -2,10 +2,7 @@ import IIIFResource from './IIIFResource.js';
import Sequence from "./Sequence.js"; import Sequence from "./Sequence.js";
import ManifestMetadata from './Metadata.js'; import ManifestMetadata from './Metadata.js';
/** class Manifest extends IIIFResource {
* @implements IIIFResource
*/
class Manifest {
id = ''; id = '';
#type = 'sc:Manifest'; #type = 'sc:Manifest';
#label = ''; #label = '';
@@ -17,9 +14,12 @@ class Manifest {
*/ */
sequences = []; sequences = [];
/**
* @param {Number} IIIFApiVersion
* @param {String} baseURL
*/
constructor(IIIFApiVersion, baseURL) { constructor(IIIFApiVersion, baseURL) {
this.context = `https://iiif.io/api/presentation/${IIIFApiVersion}/context.json`; super(IIIFApiVersion, baseURL);
this.BASE_URL = baseURL;
} }
get technique() { get technique() {
return this.#technique; return this.#technique;
@@ -46,7 +46,6 @@ class Manifest {
this.#label = `P.Herc. ${this.resourceId.split('-')[1]}`; this.#label = `P.Herc. ${this.resourceId.split('-')[1]}`;
} }
/** /**
*
* @param {ManifestMetadata} metadata * @param {ManifestMetadata} metadata
*/ */
setMetadata(metadata) { setMetadata(metadata) {

View File

@@ -1,20 +0,0 @@
{
"source": {
"include": ["src/"],
"includePattern": ".js$",
"excludePattern": "(node_modules/|docs)"
},
"plugins": ["plugins/markdown"],
"opts": {
"encoding": "utf8",
"destination": "docs/",
"recurse": true,
"verbose": true,
"template": "/home/nicolo/.npm-global/lib/node_modules/clean-jsdoc-theme",
"theme_opts": {
"theme": "light"
}
}
}

View File

@@ -0,0 +1,206 @@
'use strict;'
import { authors, COPYRIGHT } from '../constants.js';
/**
* This module parses image filenames to extract
* relevant information, according to predefined naming
* conventions.
*
* Although `parseDNFilename()` and `parseDOFilename()` are
* functionally equivalent, they're kept separate because
* it's not clear at the moment whether the naming conventions
* for the two techniques will remain equivalent.
* @module FilenameParser
*/
/**
* @typedef ParsedMetadata
* @property {String} papyrus
* @property {String} imageAuthor
* @property {String} date
* @property {String} [copyright]
*/
/**
* @typedef ImgFolderNames
* @property {String} baseFolder
* @property {String} subfolder
*/
const namePos = {
hiroxnir: 1,
nir: 1,
visr: 1,
hsi: 1,
do: 2,
dn: 2
};
/**
* @param {string} imgFilename
* @returns {ParsedMetadata}
*/
function parseHIROXNIRFilename(imgFilename) {
const prefix = imgFilename.split('_')[0];
const parts = prefix.split('-');
return {
papyrus: parts[1],
imageAuthor: authors[parts[0].replace(/\d{4}/,'')],
date: parts[0].match(/\d{4}/)[0],
}
}
/**
* @param {string} imgFilename
* @returns {ParsedMetadata}
*/
function parseNIRFilename(imgFilename) {
const prefix = imgFilename.split('_')[0];
const parts = prefix.split('-');
return {
papyrus: parts[2],
imageAuthor: authors[parts[0].replace(/\d{4}/, '')],
date: parts[0].match(/\d{4}/)[0],
copyright: COPYRIGHT.nir,
}
}
/**
* @param {string} imgFilename
* @returns {ParsedMetadata}
*/
function parseDNFilename(imgFilename) {
return {
papyrus: imgFilename.split('_')[1],
imageAuthor: '',
date: '',
copyright: COPYRIGHT.dn,
}
}
/**
* @param {string} imgFilename
* @returns {ParsedMetadata}
*/
function parseDOFilename(imgFilename) {
return {
papyrus: imgFilename.split('_')[1],
imageAuthor: '',
date: '',
copyright: COPYRIGHT.do,
}
}
/**
* @param {string} filename The image filename
* @returns {ImgFolderNames}
*/
function splitHIROXNIR(filename) {
let splitFilename = filename.split('_');
const papyrusNum = splitFilename[0].split('-')[1];
const baseFolder = `PHerc_${papyrusNum}`;
const subfolder = `PHerc_${papyrusNum}_HIROXNIR`;
return {baseFolder, subfolder};
}
/**
* @param {string} filename The image filename
* @returns {ImgFolderNames}
*/
function splitNIR(filename) {
let splitFilename = filename.split('_');
const papyrusNum = splitFilename[0].split('-')[2];
const baseFolder = `PHerc_${papyrusNum}`;
const subfolder = `PHerc_${papyrusNum}_${splitFilename[2].split('-')[0]}`;
return {baseFolder, subfolder};
}
/**
* @todo Redundant...
* @param {string} filename The image filename
* @returns {ImgFolderNames}
*/
function splitHSI(filename) {
let splitFilename = filename.split('_');
const papyrusNumb = splitFilename[0].split('-')[2];
const baseFolder = `PHerc_${papyrusNumb}`;
const subfolder = `PHerc_${papyrusNumb}_${splitFilename[2]}`;
return {baseFolder, subfolder};
}
/**
* @param {string} filename The image filename
* @returns {ImgFolderNames}
*/
function splitDNO(filename) {
let splitFilename = filename.split('_');
const papyrusNumb = splitFilename[1];
const baseFolder = `PHerc_${papyrusNumb}`;
const subfolder = `PHerc_${papyrusNumb}_${splitFilename[0]}`;
return {baseFolder, subfolder};
}
const splitters = {
hiroxnir: splitHIROXNIR,
nir: splitNIR,
visr: splitNIR,
dn: splitDNO,
do: splitDNO,
hsi: splitHSI
};
const extractors = {
nir: parseNIRFilename,
dn: parseDNFilename,
do: parseDOFilename,
hsi: parseNIRFilename,
visr: parseNIRFilename,
hiroxnir: parseHIROXNIRFilename,
}
/**
* Return metadata object from parsed
* image filename
* @param {string} imgFilename
* @param {string} technique
* @returns {ParsedMetadata}
*/
export function parse (imgFilename, technique) {
return extractors[technique](imgFilename);
}
/**
* Extract canvas name from image filename
* @param {String} imgFilename
* @param {String} technique
* @returns {String}
*/
export function getCanvasName(imgFilename, technique) {
// Remove file extension
let canvasName = imgFilename.split('_')[namePos[technique]]
.replace(/\.\w{1,3}$/, '');
if (technique === 'hsi') {
canvasName += imgFilename.split('_')[3].replace(/\..*$/,'');
}
return canvasName;
}
/**
* Generate canvas label from canvasName
* @todo Verify for HSI filenames
* @param {String} canvasName
* @returns {String}
*/
export function getCanvasLabel(canvasName) {
return canvasName.replace(
/c(\w{1,2})0+(\d+).*(\.\w{2,3})?$/i,
(str, c, number) => `C${c}. ${number}`
);
}
/**
* Get the full path (folder name) for a given image file
* @param {String} filename
* @param {String} technique
* @returns {ImgFolderNames}
*/
export function getImagePath(filename, technique) {
return splitters[technique](filename);
}

View File

@@ -0,0 +1,61 @@
'use strict';
import * as fs from 'fs';
/**
* Handles filesystem for image repository
* @module ImageRepository
*/
/**
* @param {string} manifestId
* @returns {string[]}
* @throws `readdir` will thrown an ENOENT error if the images folder doesn't exist.
* The manifest route should catch it.
*/
export async function getImageList (manifestId) {
// Regex to exclude images with certain patterns in filename
const regexFilter = new RegExp(/(c2r|copertin.|camice|tit)/, 'i');
let folderName = manifestId.replace(/pherc-(\d+)-(\w+)$/, function (_match, g1, g2) {
return `PHerc_${g1}_${g2.toUpperCase()}`;
});
let baseFolder = `${folderName.split('_')[0]}_${folderName.split('_')[1]}`;
let files = await fs.promises.readdir(
`${process.env.IMAGES_DIR}/${baseFolder}/${folderName}`
);
files = files.filter(file => !regexFilter.test(file) && !file.startsWith('.'));
return files;
}
/**
* Retrieves available image techniques
* for all papyruses based on folder contents
* @returns {object}
*/
export async function getParamsFromFolders () {
let params = [];
const papyri = await fs.promises.readdir(process.env.IMAGES_DIR);
for (let p of papyri) {
let techniques = [];
for (let tech of await fs.promises.readdir(`${process.env.IMAGES_DIR}/${p}`)) {
let files = await fs.promises.readdir(
`${process.env.IMAGES_DIR}/${p}/${tech}`
);
files = files.filter(file => /(tiff?|jpe?g|jp2|bmp)/.test(file));
if (files.length) {
techniques.push(tech.replace(/PHerc_\d+_/i, ''));
}
}
params.push({
name : p.replace('_', ' '),
techniques
});
}
return params;
}

View File

@@ -0,0 +1,176 @@
'use strict';
import Manifest from '../iiif/Manifest.js';
import Sequence from '../iiif/Sequence.js';
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 { TECH_NAMES } from '../constants.js';
/**
* Builds a manifest object based on canvases
* and available metadata
* @module ManifestBuilder
*/
const IIIF_API_VERSION = process.env.IIIF_API_VERSION;
const BASE_URL = process.env.BASE_URL;
const IMAGE_SERVER_URL = process.env.IMAGE_SERVER_URL;
/**
* Builds a Manifest object
* @param {String} manifestId
* @returns {Object}
*/
export async function buildManifest(manifestId) {
let manifest = new Manifest(IIIF_API_VERSION, BASE_URL);
manifest.generateID(manifestId);
manifest.generateLabel();
const images = await getImageList(manifestId);
manifest = await populateCanvases(manifest, images, manifestId);
manifest.setMetadata(
createMetadata(
manifest,
images[0], // A single image filename is sufficient to extract metadata
)
);
return manifest.toObject();
}
/**
* Builds a Canvas object from route parameters
* @param {String} manifestId
* @param {String} name The canvas name
*/
export async function buildCanvas(manifestId, name) {
const manifest = new Manifest(IIIF_API_VERSION, BASE_URL);
manifest.generateID(manifestId);
let filename = await getImageName(name, manifestId);
return createCanvas(manifest, filename);
}
/**
* Create a canvas from an image filename
* @param {Manifest} manifest
* @param {string} filename The image filename
* @returns {Canvas}
*/
async function createCanvas (manifest, filename) {
let canvas = new Canvas(IIIF_API_VERSION, BASE_URL);
const canvasName = getCanvasName(filename, manifest.technique);
canvas.generateID(manifest.resourceId, canvasName.toLowerCase());
canvas.label = getCanvasLabel(canvasName);
let image = new Image(canvas.id);
image.generateID(IMAGE_SERVER_URL, filename, manifest.technique);
const imgSize = await getImageSize(image.id);
image.setSize(imgSize.height, imgSize.width);
canvas.setThumbnail(
imgSize.thumb.height,
imgSize.thumb.width,
image.id
);
canvas.addImage(image);
return canvas;
}
/**
* @param {string} imageId The image's id as a URL to the image server
* @returns {{width: number, height: number, thumb: {width: number, height: number}}}
*/
async function getImageSize(imageId) {
let infoURL = imageId.replace(/full.*$/,'info.json');
const res = await fetch(infoURL);
let size = {};
if (res.ok) {
const infoJson = await res.json();
const maxSize = infoJson.sizes[infoJson.sizes.length - 1];
size.height = maxSize.height;
size.width = maxSize.width;
size.thumb = {
width: infoJson.sizes[1].width,
height: infoJson.sizes[1].height,
}
}
return size;
}
/**
* @param {Manifest} manifest The manifest object
* @param {string[]} images List of image filenames from folder
* @returns {Manifest}
*/
async function populateCanvases (manifest, images) {
const sequence = new Sequence(BASE_URL);
// There's only one sequence
sequence.generateID(manifest.resourceId, 0);
for (let img of images) {
// Skip failing images (TODO log error to file)
try {
let canvas = await createCanvas(manifest, img);
sequence.addCanvas(canvas);
} catch (error) {
console.error(error);
console.log(`\nAffected image: ${img}`);
continue;
}
}
// Sort them according to their ID number... (cornice, colonna, ecc...)
sequence.canvases.sort((a, b) => {
const firstId = a['@id'].slice(a['@id'].lastIndexOf('/') + 1).replace(/[a-z]+/ig,'');
const secondId = b['@id'].slice(b['@id'].lastIndexOf('/') + 1).replace(/[a-z]+/ig,'');
return Number(firstId) - Number(secondId);
});
manifest.addSequence(sequence);
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
* @returns {ManifestMetadata}
*/
function createMetadata(manifest, imgFilename) {
let metadata = parse(imgFilename, manifest.technique);
metadata.technique = TECH_NAMES[manifest.technique];
return new ManifestMetadata(metadata);
}

View File

@@ -232,6 +232,14 @@ cookie@0.5.0:
resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz"
integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw== integrity sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==
cors@^2.8.6:
version "2.8.6"
resolved "https://registry.yarnpkg.com/cors/-/cors-2.8.6.tgz#ff5dd69bd95e547503820d29aba4f8faf8dfec96"
integrity sha512-tJtZBBHA6vjIAaF6EnIaq6laBBP9aq/Y3ouVJjEfoHbRBcHBAHYcMh/w8LDrk2PvIMMq8gmopa5D4V8RmbrxGw==
dependencies:
object-assign "^4"
vary "^1"
debug@2.6.9: debug@2.6.9:
version "2.6.9" version "2.6.9"
resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz" resolved "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz"
@@ -576,6 +584,11 @@ no-case@^3.0.4:
lower-case "^2.0.2" lower-case "^2.0.2"
tslib "^2.0.3" tslib "^2.0.3"
object-assign@^4:
version "4.1.1"
resolved "https://registry.yarnpkg.com/object-assign/-/object-assign-4.1.1.tgz#2109adc7965887cfc05cbbd442cac8bfbb360863"
integrity sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==
object-inspect@^1.9.0: object-inspect@^1.9.0:
version "1.12.3" version "1.12.3"
resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz" resolved "https://registry.npmjs.org/object-inspect/-/object-inspect-1.12.3.tgz"
@@ -794,7 +807,7 @@ utils-merge@1.0.1:
resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz" resolved "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz"
integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==
vary@~1.1.2: vary@^1, vary@~1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz"
integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg== integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==