diff --git a/.env.example b/.env.example index fa97aed..cb006f8 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ +DEBUG='dev|prod' PORT = 8080 BASE_URL = 'https://something.com' # The most recent API version number that should be supported diff --git a/README.md b/README.md new file mode 100644 index 0000000..9e3eaf3 --- /dev/null +++ b/README.md @@ -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`. diff --git a/app.mjs b/app.mjs index 0f7537a..a34dd75 100644 --- a/app.mjs +++ b/app.mjs @@ -1,12 +1,10 @@ +import 'dotenv/config'; import createError from 'http-errors'; import logger from 'morgan'; +import cors from 'cors'; import express from 'express'; -import path from 'path'; -import * as dotenv from 'dotenv'; import router from './routes/index.mjs'; -dotenv.config(); - let indexRouter = router; let app = express(); @@ -17,10 +15,13 @@ const PORT = process.env.PORT || 3000; //app.set('views', path.join(__dirname, 'views')); 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.urlencoded({ extended: false })); -//app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); diff --git a/controllers/canvas.mjs b/controllers/canvas.mjs index 7101561..20888ad 100644 --- a/controllers/canvas.mjs +++ b/controllers/canvas.mjs @@ -1,7 +1,5 @@ -import Canvas from '../src/Canvas.js'; -import Image from '../src/Image.js'; -import Manifest from '../src/Manifest.js'; -import Common from '../src/common.js'; +import { buildCanvas } from '../src/service/ManifestBuilder.js'; + /** * Generate a canvas object to serve * @todo Use createCanvas from Common? @@ -10,47 +8,5 @@ import Common from '../src/common.js'; * @param {number|string} name The canvas name */ export default async function generateCanvas(manifestId, name) { - const IIIF_API_VERSION = process.env.IIIF_API_VERSION; - 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(); - */ + return buildCanvas(manifestId, name); } \ No newline at end of file diff --git a/controllers/manifest.mjs b/controllers/manifest.mjs index 10e5f49..9578797 100644 --- a/controllers/manifest.mjs +++ b/controllers/manifest.mjs @@ -1,34 +1,12 @@ '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 * @param {string} manifestId */ export default async function generateManifest(manifestId) { - const IIIF_API_VERSION = process.env.IIIF_API_VERSION; - 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(); + return buildManifest(manifestId); } diff --git a/controllers/params.mjs b/controllers/params.mjs index 56c585a..4f78fba 100644 --- a/controllers/params.mjs +++ b/controllers/params.mjs @@ -1,22 +1,20 @@ '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 */ export default async function exposeParams() { console.log(process.env.IMAGES_DIR); - let techs = []; - for (let key in Common.TECH_NAMES) { - techs.push({ - 'acronym': key.toUpperCase(), - 'fullname': Common.TECH_NAMES[key] - }); - } + const techs = Object.entries(TECH_NAMES).map(([key, fullname]) => ({ + acronym: key.toUpperCase(), + fullname, + })); - let papyri = await Common.getParamsFromFolders(); + let papyri = await getParamsFromFolders(); return { 'techniques' : techs, diff --git a/jsdoc.json b/jsdoc.json index 9af3a7d..250fd3f 100644 --- a/jsdoc.json +++ b/jsdoc.json @@ -4,6 +4,10 @@ }, "opts": { + "plugins": ["plugins/markdown"], + "markdown": { + "tags": ["module", "namespace"] + }, "encoding": "utf8", "destination": "docs/", "recurse": true, diff --git a/package.json b/package.json index b66fe81..791ce74 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,7 @@ { "dependencies": { "clean-jsdoc-theme": "^4.2.13", + "cors": "^2.8.6", "dotenv": "^16.3.1", "ejs": "^3.1.9", "express": "^4.18.2", diff --git a/routes/index.mjs b/routes/index.mjs index fc7af80..f12e83a 100644 --- a/routes/index.mjs +++ b/routes/index.mjs @@ -11,15 +11,18 @@ let router = express.Router(); /* GET manifest JSON */ router.get('/iiif/:manifestid/manifest', async function(req, res) { let manifest = {}; - res.set('Access-Control-Allow-Origin', '*'); try { manifest = await generateManifest(req.params.manifestid) } catch(error) { + + console.debug(error); + res.status(500).json({ status: 500, message: 'There was an error processing this request', code: error.code ?? 'not available', }); + return; } res.json(manifest); }); @@ -27,7 +30,6 @@ router.get('/iiif/:manifestid/manifest', async function(req, res) { /* GET canvas JSON */ router.get('/iiif/:manifestid/canvas/:name', async function(req, res) { let canvas = {}; - res.set('Access-Control-Allow-Origin', '*'); try { canvas = await generateCanvas(req.params.manifestid, req.params.name) } catch(error) { @@ -44,7 +46,6 @@ router.get('/iiif/:manifestid/canvas/:name', async function(req, res) { /* GET sequence JSON */ router.get('/iiif/:manifestid/sequence/:name', async function(req, res) { let sequence = {}; - res.set('Access-Control-Allow-Origin', '*'); try { sequence = await generateSequence(req.params.manifestid, req.params.name) } catch(error) { @@ -53,13 +54,13 @@ router.get('/iiif/:manifestid/sequence/:name', async function(req, res) { message: 'There was an error processing this request', code: error.code ?? 'not available', }); + return; } res.json(sequence); }); /* GET possible params for manifest responses */ router.get('/params', async function(req, res) { - res.set('Access-Control-Allow-Origin', '*'); try { res.json(await exposeParams()); } catch(error) { diff --git a/src/IIIFResource.js b/src/IIIFResource.js deleted file mode 100644 index 6a66551..0000000 --- a/src/IIIFResource.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * @interface - */ -class IIIFResource { - id; - type; - - generateId(serviceURL, filename) {} - toObject() {} -} - -export default IIIFResource; \ No newline at end of file diff --git a/src/common.js b/src/common.js deleted file mode 100644 index be5f479..0000000 --- a/src/common.js +++ /dev/null @@ -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; diff --git a/src/constants.js b/src/constants.js new file mode 100644 index 0000000..6a9214d --- /dev/null +++ b/src/constants.js @@ -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)", +}; \ No newline at end of file diff --git a/src/Canvas.js b/src/iiif/Canvas.js similarity index 78% rename from src/Canvas.js rename to src/iiif/Canvas.js index ca6c55a..cca1d36 100644 --- a/src/Canvas.js +++ b/src/iiif/Canvas.js @@ -1,27 +1,30 @@ import IIIFResource from './IIIFResource.js'; import Image from './Image.js'; -/** - * @implements IIIFResource - */ -class Canvas { +import ManifestMetadata from './Metadata.js'; + +class Canvas extends IIIFResource { id = ''; #type = 'sc:Canvas'; #label = ''; + #metadata = {}; resourceId = ''; name = ''; images = []; thumbnail = {}; + /** + * @param {Number} IIIFApiVersion + * @param {String} baseURL + */ constructor(IIIFApiVersion, baseURL) { - this.context = `https://iiif.io/api/presentation/${IIIFApiVersion}/context.json`; - this.BASE_URL = baseURL; + super(IIIFApiVersion, baseURL); } /** * @param {string} resourceId The resource ID for this canvas * @param {int|string} name A unique name for this canvas */ 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.resourceId = resourceId; this.name = name; @@ -53,6 +56,12 @@ class Canvas { width } } + /** + * @param {ManifestMetadata} metadata + */ + setMetadata(metadata) { + this.#metadata = metadata.toObject(); + } /** * Object representation * @returns {object} @@ -63,6 +72,7 @@ class Canvas { "@id" : this.id, "@type" : this.#type, "label" : this.#label, + "metadata" : this.#metadata, "images" : this.images, "thumbnail" : this.thumbnail } diff --git a/src/iiif/IIIFResource.js b/src/iiif/IIIFResource.js new file mode 100644 index 0000000..e65ccdd --- /dev/null +++ b/src/iiif/IIIFResource.js @@ -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; \ No newline at end of file diff --git a/src/Image.js b/src/iiif/Image.js similarity index 56% rename from src/Image.js rename to src/iiif/Image.js index de98248..ee65f48 100644 --- a/src/Image.js +++ b/src/iiif/Image.js @@ -1,53 +1,8 @@ +'use strict;' + +import { getImagePath } from '../service/FilenameParser.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 */ @@ -87,11 +42,10 @@ class Image { * server endpoint for this image * @param {string} serviceURL The image server base URL * @param {string} filename The image's complete filename + * @param {string} technique The imaging technique (from the manifest) */ - generateID(serviceURL, filename) { - let splitFn = splitter[/((HIROX)?NIR|DO|DN|HSI)/.exec(filename)[0]]; - - const {baseFolder, subfolder} = splitFn(filename); + generateID(serviceURL, filename, technique) { + const {baseFolder, subfolder} = getImagePath(filename, technique); this.id = `${serviceURL}/${this.#IIIF_API_VERSION}/${baseFolder}%2F${subfolder}%2F${filename}/full/max/0/default.jpg`; this.service['@id'] = this.id.replace(/\/full.*$/,''); diff --git a/src/Manifest.js b/src/iiif/Manifest.js similarity index 89% rename from src/Manifest.js rename to src/iiif/Manifest.js index a0ad040..1d26bdc 100644 --- a/src/Manifest.js +++ b/src/iiif/Manifest.js @@ -2,10 +2,7 @@ import IIIFResource from './IIIFResource.js'; import Sequence from "./Sequence.js"; import ManifestMetadata from './Metadata.js'; -/** - * @implements IIIFResource - */ -class Manifest { +class Manifest extends IIIFResource { id = ''; #type = 'sc:Manifest'; #label = ''; @@ -17,9 +14,12 @@ class Manifest { */ sequences = []; + /** + * @param {Number} IIIFApiVersion + * @param {String} baseURL + */ constructor(IIIFApiVersion, baseURL) { - this.context = `https://iiif.io/api/presentation/${IIIFApiVersion}/context.json`; - this.BASE_URL = baseURL; + super(IIIFApiVersion, baseURL); } get technique() { return this.#technique; @@ -46,7 +46,6 @@ class Manifest { this.#label = `P.Herc. ${this.resourceId.split('-')[1]}`; } /** - * * @param {ManifestMetadata} metadata */ setMetadata(metadata) { diff --git a/src/Metadata.js b/src/iiif/Metadata.js similarity index 100% rename from src/Metadata.js rename to src/iiif/Metadata.js diff --git a/src/Sequence.js b/src/iiif/Sequence.js similarity index 100% rename from src/Sequence.js rename to src/iiif/Sequence.js diff --git a/src/jsdoc.json b/src/jsdoc.json deleted file mode 100644 index e9a29a3..0000000 --- a/src/jsdoc.json +++ /dev/null @@ -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" - } - } -} diff --git a/src/service/FilenameParser.js b/src/service/FilenameParser.js new file mode 100644 index 0000000..e0c0937 --- /dev/null +++ b/src/service/FilenameParser.js @@ -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); +} \ No newline at end of file diff --git a/src/service/ImageRepository.js b/src/service/ImageRepository.js new file mode 100644 index 0000000..ba6c333 --- /dev/null +++ b/src/service/ImageRepository.js @@ -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; +} \ No newline at end of file diff --git a/src/service/ManifestBuilder.js b/src/service/ManifestBuilder.js new file mode 100644 index 0000000..d32fe29 --- /dev/null +++ b/src/service/ManifestBuilder.js @@ -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); +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index d54f24a..a08d793 100644 --- a/yarn.lock +++ b/yarn.lock @@ -232,6 +232,14 @@ cookie@0.5.0: resolved "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz" 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: version "2.6.9" 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" 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: version "1.12.3" 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" integrity sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA== -vary@~1.1.2: +vary@^1, vary@~1.1.2: version "1.1.2" resolved "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz" integrity sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==