Substantial refactoring of code structure
This commit is contained in:
@@ -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
5
README.md
Normal 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
13
app.mjs
@@ -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);
|
||||||
|
|
||||||
|
|||||||
@@ -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();
|
|
||||||
*/
|
|
||||||
}
|
}
|
||||||
@@ -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();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
/**
|
|
||||||
* @interface
|
|
||||||
*/
|
|
||||||
class IIIFResource {
|
|
||||||
id;
|
|
||||||
type;
|
|
||||||
|
|
||||||
generateId(serviceURL, filename) {}
|
|
||||||
toObject() {}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IIIFResource;
|
|
||||||
292
src/common.js
292
src/common.js
@@ -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
24
src/constants.js
Normal 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)",
|
||||||
|
};
|
||||||
@@ -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
19
src/iiif/IIIFResource.js
Normal 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;
|
||||||
@@ -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.*$/,'');
|
||||||
@@ -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) {
|
||||||
@@ -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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
206
src/service/FilenameParser.js
Normal file
206
src/service/FilenameParser.js
Normal 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);
|
||||||
|
}
|
||||||
61
src/service/ImageRepository.js
Normal file
61
src/service/ImageRepository.js
Normal 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;
|
||||||
|
}
|
||||||
176
src/service/ManifestBuilder.js
Normal file
176
src/service/ManifestBuilder.js
Normal 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);
|
||||||
|
}
|
||||||
15
yarn.lock
15
yarn.lock
@@ -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==
|
||||||
|
|||||||
Reference in New Issue
Block a user