Ver Fonte

Sharp library

isundil há 1 ano atrás
pai
commit
3c7455135e

+ 3 - 2
model/mediaService.js

@@ -26,11 +26,12 @@ MediaStruct.prototype.pushMeta = function(key, value) {
             'photochamberImport', 'height', 'width', 'iso', 'focal',
             'fNumber', 'exposureTime', 'camera', 'lensModel',
             'exposureTimeStr', 'libraryPath', 'compression',
-            'software', 'fileSize', 'geoHash', 'exposureProgram'
+            'software', 'fileSize', 'geoHash', 'exposureProgram',
+            'orientation'
         ];
         if (['photochamberImport', 'dateTime'].indexOf(key) >= 0)
             type = 'date';
-        else if (['height', 'width', 'iso', 'focal', 'fNumber', 'exposureTime'].indexOf(key) >= 0)
+        else if (['height', 'width', 'iso', 'focal', 'fNumber', 'exposureTime', 'orientation'].indexOf(key) >= 0)
             type = 'number';
         else if (['geoHash', 'gpsLocation'].indexOf(key) >= 0)
             type = 'geoData';

+ 2 - 0
package.json

@@ -21,6 +21,7 @@
     "bootstrap-slider": "^11.0.2",
     "craftlabhttpserver": "git:isundil/craftlabHttpServer",
     "crypto": "^1.0.1",
+    "exif-reader": "^2.0.1",
     "geokit": "^1.1.0",
     "imagemagick": "^0.1.3",
     "ldap": "^0.7.1",
@@ -29,6 +30,7 @@
     "mime-types": "^2.1.35",
     "node-simple-router": "^0.10.2",
     "offline-geocoder": "^1.0.0",
+    "sharp": "^0.33.3",
     "sqlite3": "^5.1.6",
     "tmp": "^0.2.1",
     "whiskers": "^0.4.0"

+ 5 - 1
router/api.js

@@ -116,8 +116,12 @@ module.exports = { register: app => {
             return app.routerUtils.onPageNotFound(res);
         try {
             let thumbnail = null;
+            req.body = req.body || {};
+            req.body.w = parseInt(req.body.w || 0);
+            req.body.h = parseInt(req.body.h || 0);
+            req.body.q = parseInt(req.body.q || 6);
             try {
-                thumbnail = await (await app.libraryManager.findMedia(data.path))?.createThumbnail(req.body?.w || 0, req.body?.h || 0, req.body?.q || 6);
+                thumbnail = await (await app.libraryManager.findMedia(data.path))?.createThumbnail(req.body.w, req.body.h, req.body.q);
             } catch (err) {
                 return app.routerUtils.apiError(res);
             }

+ 9 - 3
src/fileTypeManager.js

@@ -1,5 +1,6 @@
 
 const parsers = [
+    require ('./filetype/sharp.js'),
     require ('./filetype/imagemagick.js'),
     require ('./filetype/meta.js')
 ];
@@ -16,11 +17,16 @@ module.exports.createThumbnail = async function(fileObj, w, h, quality) {
 
 module.exports.createMeta = async function(fileObj) {
     let result = {};
-    for (let i of await Promise.allSettled(parsers.map(i => i.parse(fileObj))))
+    for (let i of parsers)
     {
-        i.value && Object.assign(result, i.value);
-        i.reason && console.error("Failed to parse file ", fileObj.path, i.reason);
+        let data = await i.parse(fileObj, Object.assign({}, result));
+        data.value && Object.assign(result, data.value);
+        data.reason && console.error("Failed to parse file ", fileObj.path, data.reason);
     }
+    for (let i of Object.keys(result))
+        if (result[i] === undefined)
+            delete result[i];
+    delete result["exifParsed"];
     return result;
 }
 

+ 40 - 79
src/filetype/imagemagick.js

@@ -1,9 +1,35 @@
 
 const fs = require('fs');
 const tmp = require('tmp');
-const im = require('./../imagemagickWrapper.js');
-const geokit = require('geokit');
-const geocoder = require('offline-geocoder')({ database: 'static/db.sqlite'});
+const imLib = require('imagemagick');
+const ThreadPool = require('../threadPool.js');
+const MetaStruct = require('./metaStruct.js').MetaStruct;
+
+class ImagemagickWrapper {
+    #threadPool = new ThreadPool(5);
+
+    readMeta(path) {
+        return this.#threadPool.pushTask(() => new Promise((ok, ko) => {
+            imLib.identify(['-format', '%[EXIF:*]Compression=%[compression]\nWidth=%w\nHeight=%h\n', path], (err, stdout) => {
+                if (err)
+                    return ko(err);
+                ok(stdout);
+            });
+        }));
+    }
+
+    convert(args) {
+        return this.#threadPool.pushTask(() => new Promise((ok, ko) => {
+            imLib.convert(args, (err, stdout) => {
+                if (err)
+                    return ko(err);
+                ok(stdout);
+            });
+        }));
+    }
+}
+
+const im = new ImagemagickWrapper();
 
 function readMeta(path) {
     return new Promise(async (ok, ko) => {
@@ -46,63 +72,6 @@ function exifSlash(value) {
     return eval(value);
 }
 
-function ExifGps(data) {
-    /*
-     *    gPSLatitude: '46/1,54/1,3500/100',
-          gPSLatitudeRef: 'N',
-          gPSLongitude: '23/1,48/1,3614/100',
-          gPSLongitudeRef: 'E',
-    */
-    this.data = null;
-    if (!data.gPSLatitude || !data.gPSLongitude || !(/^[0-9\-,\/]+$/).test(data.gPSLatitude) || !(/^[0-9\-,\/]+$/).test(data.gPSLongitude))
-        return;
-    try
-    {
-        this.data = {
-            lat: data.gPSLatitude.split(',').map(eval),
-            lon: data.gPSLongitude.split(',').map(eval)
-        };
-    }
-    catch(err)
-    {
-        this.data = null;
-        return;
-    }
-    if (data.gPSLatitudeRef && data.gPSLatitudeRef !== 'N' && this.data.lat[0] >= 0)
-        this.data.lat[0] *= -1;
-    if (data.gPSLongitudeRef && data.gPSLongitudeRef !== 'E' && this.data.lon[0] >= 0)
-        this.data.lon[0] *= -1;
-    this.data.lat = this.data.lat[0] + this.data.lat[1] / 60 + this.data.lat[2]/3600;
-    this.data.lon = this.data.lon[0] + this.data.lon[1] / 60 + this.data.lon[2]/3600;
-    if (!this.data.lat || isNaN(this.data.lat) || !this.data.lon || isNaN(this.data.lon))
-        this.data = null;
-}
-
-ExifGps.prototype.toGps = function() {
-    if (!this.data)
-        return undefined;
-    return JSON.stringify([this.data.lat, this.data.lon]);
-}
-
-ExifGps.prototype.toGeoHash = function() {
-    if (!this.data)
-        return undefined;
-    return geokit.hash({lat: this.data.lat, lng: this.data.lon});
-}
-
-ExifGps.prototype.toAddress = function() {
-    return new Promise(async (ok, ko) => {
-        if (!this.data)
-            return ok(undefined);
-        const data = await geocoder.reverse(this.data.lat, this.data.lon);
-        ok({
-            city: data?.name,
-            admin: data?.admin1?.name,
-            country: data?.country?.name
-        });
-    });
-}
-
 function readTags(data) {
     let result = [];
     if (data['winXP-Keywords']) {
@@ -111,23 +80,16 @@ function readTags(data) {
     return result;
 }
 
-function exposureProgram(programId) {
-    if (isNaN(programId) || !programId)
-        return;
-    return [ "Manual", "Normal program", "Aperture priority", "Shutter priority",
-        "Creative program", "Action program", "Portrait mode", "Landscape mode"][programId];
-}
-
-module.exports.parse = async (fileObj) => {
-    if (!fileObj.mimeType.startsWith('image/'))
+module.exports.parse = async (fileObj, data) => {
+    if (!fileObj.mimeType.startsWith('image/') || data["exifParsed"])
         return {};
-    let result = {};
+    let result = new MetaStruct();
     try {
         let imdata = await readMeta(fileObj.path);
         if (!imdata)
             return {};
         result.artist = imdata.artist || undefined;
-        result.exposureProgram = exposureProgram(Number.parseInt(imdata.exposureProgram));
+        result.setExposureProgram(Number.parseInt(imdata.exposureProgram));
         result.exposureTime = exifSlash(imdata.exposureTime);
         result.exposureTimeStr = imdata.exposureTime || undefined;
         result.dateTime = exifDate(imdata.dateTimeDigitized || imdata.dateTimeOriginal);
@@ -140,13 +102,12 @@ module.exports.parse = async (fileObj) => {
         result.width = imdata.width || undefined;
         result.height = imdata.height || undefined;
         result.compression = imdata.compression || undefined;
-        const gpsData = new ExifGps(imdata);
-        result.gpsLocation = gpsData.toGps();
-        result.geoHash = gpsData.toGeoHash();
-        const address = await gpsData.toAddress();
-        result.geoCountry = address?.country;
-        result.geoAdmin = address?.admin;
-        result.geoCity = address?.city;
+        result.orientation = imdata.orientation || undefined;
+        try {
+            result.setGPSInfo(imdata.gPSLatitude.split(',').map(eval), data.gPSLatitudeRef,
+                imdata.gPSLongitude.split(',').map(eval), data.gPSLongitudeRef);
+        }
+        catch (err) {}
         result.tags = readTags(imdata);
     }
     catch (err) {
@@ -155,7 +116,7 @@ module.exports.parse = async (fileObj) => {
     for (let i of Object.keys(result))
         if (result[i] === undefined || result[i].length === 0)
             delete result[i];
-    return result;
+    return { value: result };
 }
 
 module.exports.createThumbnail = async (fileObj, width, height, quality) => {

+ 93 - 0
src/filetype/metaStruct.js

@@ -0,0 +1,93 @@
+
+const geokit = require('geokit');
+const geocoder = require('offline-geocoder')({ database: 'static/db.sqlite'});
+
+function MetaStruct() {
+    this.artist = undefined;
+    this.exposureProgram = undefined;
+    this.exposureTime = undefined;
+    this.exposureTimeStr = undefined;
+    this.dateTime = undefined;
+    this.fNumber = undefined;
+    this.focal = undefined;
+    this.lensModel = undefined;
+    this.camera = undefined;
+    this.software = undefined;
+    this.iso = undefined;
+    this.width = undefined;
+    this.height = undefined;
+    this.compression = undefined;
+    this.gpsLocation = undefined;
+    this.geoHash = undefined;
+    this.geoCountry = undefined;
+    this.geoAdmin = undefined;
+    this.geoCity = undefined;
+    this.orientation = undefined;
+    this.tags = undefined;
+
+    // Internal
+    this.imException = undefined;
+    this.exifParsed = false;
+}
+
+MetaStruct.prototype.setExposureProgram = function(progId) {
+    if (isNaN(progId) || !progId)
+        this.exposureProgram = undefined;
+    else
+        this.exposureProgram = [ "Manual", "Normal program", "Aperture priority", "Shutter priority",
+            "Creative program", "Action program", "Portrait mode", "Landscape mode"][progId];
+}
+
+function ExifGps(lat, latRef, lon, lonRef) {
+    this.lat = null;
+    this.lon = null;
+
+    if (!lat || !lon)
+        return;
+    if (latRef && latRef !== 'N' && lat >= 0)
+        lat[0] *= -1;
+    if (lonRef && lonRef !== 'E' && lon[0] >= 0)
+        lon[0] *= -1;
+    this.lat = lat[0] + lat[1] / 60 + lat[2]/3600;
+    this.lon = lon[0] + lon[1] / 60 + lon[2]/3600;
+    if (!this.lat || isNaN(this.lat) || !this.lon || isNaN(this.lon))
+        this.lat = this.lon = null;
+}
+
+ExifGps.prototype.toGps = function() {
+    if (this.lat === null)
+        return undefined;
+    return JSON.stringify([this.lat, this.lon]);
+}
+
+ExifGps.prototype.toGeoHash = function() {
+    if (this.lat === null)
+        return undefined;
+    return geokit.hash({lat: this.lat, lng: this.lon});
+}
+
+ExifGps.prototype.toAddress = function() {
+    return new Promise(async (ok, ko) => {
+        if (this.lat === null)
+            return ok(undefined);
+        const data = await geocoder.reverse(this.lat, this.lon);
+        ok({
+            city: data?.name,
+            admin: data?.admin1?.name,
+            country: data?.country?.name
+        });
+    });
+}
+
+MetaStruct.prototype.setGPSInfo = async function(latitude, latitudeRef, longitude, longitudeRef) {
+    let gpsData = new ExifGps(latitude, latitudeRef, longitude, longitudeRef);
+    this.gpsLocation = gpsData.toGps();
+    this.geoHash = gpsData.toGeoHash();
+    const address = await gpsData.toAddress();
+    this.geoCountry = address?.country;
+    this.geoAdmin = address?.admin;
+    this.geoCity = address?.city;
+}
+
+module.exports.MetaStruct = MetaStruct;
+

+ 90 - 0
src/filetype/sharp.js

@@ -0,0 +1,90 @@
+
+const tmp = require('tmp');
+const Sharp = require('sharp');
+const ThreadPool = require('../threadPool.js');
+const MetaStruct = require('./metaStruct.js').MetaStruct;
+const exifReader = require('exif-reader');
+
+class SharpWrapper {
+    #threadPool = new ThreadPool(15);
+
+    parse(path) {
+        return this.#threadPool.pushTask(() =>
+            new Sharp(path)
+                .metadata()
+        );
+    }
+
+    async convert(fileObj, width, height, quality) {
+        const output = tmp.fileSync({ discardDescriptor: true });
+
+        let result = null;
+        try {
+            result = await this.#threadPool.pushTask(() =>
+                new Sharp(fileObj.path)
+                    .resize(width, height, { fit: "inside", withoutEnlargement: true })
+                    .rotate()
+                    .jpeg({ quality: quality *10 })
+                    .toFile(output.name)
+            );
+        } catch (err) {
+            console.error("Sharp::createThumbnail: ", err);
+            output.removeCallback();
+            result = null;
+        }
+        return result ? output : null;
+    }
+}
+
+const sh = new SharpWrapper();
+
+async function sharpParse(path) {
+    let rawData = await sh.parse(path);
+    let result = new MetaStruct();
+
+    result.compression = rawData.format;
+    result.width = rawData.width;
+    result.height = rawData.height;
+    result.exifParsed = 0;
+
+    if (!rawData.exif)
+        return;
+    let exifData = exifReader(rawData.exif);
+    result.setExposureProgram(exifData?.Photo?.ExposureProgram);
+    result.exposureTime = exifData?.Photo?.ExposureTime || undefined;
+    result.exposureTimeStr = undefined;
+    result.dateTime = exifData?.Photo?.DateTimeOriginal || exifData?.Image?.DateTime || undefined;
+    result.fNumber = exifData?.Photo?.FNumber || undefined;
+    result.focal = exifData?.Photo?.FocalLength || undefined;
+    result.lensModel = exifData?.Photo?.LensModel || undefined;
+    result.camera = ((exifData?.Image?.Model || "") + (exifData?.Image?.Model && exifData?.Image.Make ? " " : "") + (exifData?.Image?.Make || "")) || undefined;
+    result.artist = exifData?.Image?.Artist || undefined;
+    result.software = exifData?.Image?.Software || undefined;
+    result.iso = exifData?.Photo?.ISOSpeedRatings || undefined;
+    result.orientation = exifData?.Image?.Orientation || undefined;
+    result.tags = [];
+    try {
+        if (exifData?.Image?.XPKeywords)
+            result.tags = Buffer.from(exifData.Image.XPKeywords.filter(x => !!x)).toString().split(";");
+    } catch (err) {}
+
+    if (exifData?.GPSInfo?.GPSLatitudeRef && exifData?.GPSInfo?.GPSLatitude &&
+        exifData?.GPSInfo?.GPSLongitudeRef && exifData?.GPSInfo?.GPSLongitude) {
+        await result.setGPSInfo(exifData?.GPSInfo?.GPSLatitude, exifData?.GPSInfo?.GPSLatitudeRef, exifData?.GPSInfo?.GPSLongitude, exifData?.GPSInfo?.GPSLongitudeRef);
+    }
+    result.exifParsed = 1;
+    return result;
+}
+
+module.exports.parse = async (fileObj) => {
+    try {
+        return { value: await sharpParse(fileObj.path) };
+    } catch (err) {
+        return { reason: err };
+    }
+}
+
+module.exports.createThumbnail = (fileObj, width, height, quality) => {
+    return sh.convert(fileObj, width, height, quality);
+};
+

+ 0 - 30
src/imagemagickWrapper.js

@@ -1,30 +0,0 @@
-
-const im = require('imagemagick');
-const ThreadPool = require('./threadPool.js');
-
-class ImagemagickWrapper {
-    #threadPool = new ThreadPool(5);
-
-    readMeta(path) {
-        return this.#threadPool.pushTask(() => new Promise((ok, ko) => {
-            im.identify(['-format', '%[EXIF:*]Compression=%[compression]\nWidth=%w\nHeight=%h\n', path], (err, stdout) => {
-                if (err)
-                    return ko(err);
-                ok(stdout);
-            });
-        }));
-    }
-
-    convert(args) {
-        return this.#threadPool.pushTask(() => new Promise((ok, ko) => {
-            im.convert(args, (err, stdout) => {
-                if (err)
-                    return ko(err);
-                ok(stdout);
-            });
-        }));
-    }
-}
-
-module.exports = new ImagemagickWrapper();
-

+ 2 - 2
src/threadPool.js

@@ -28,10 +28,10 @@ class ThreadPool {
                 this.#activeTask--;
                 ok(result);
             })
-            .catch(() => {
+            .catch(err => {
                 let ko = activeTask.ko;
                 this.#activeTask--;
-                ko(taskResult.result);
+                ko(err);
             })
             .finally(() => {
                 this.#runNextTask();

+ 3 - 0
static/public/css/style.css

@@ -74,6 +74,8 @@ body.filter-active #pch-navbar .bt-filter-inactive {
 #pch-mediaList > .pch-image img {
     transition: scale 300ms;
     object-fit: contain;
+    max-height: 100%;
+    max-width: 100%;
 }
 
 #pch-mediaList > .pch-image:hover img {
@@ -193,6 +195,7 @@ body.filter-active #pch-navbar .bt-filter-inactive {
     max-height: 100%;
     display: inline-flex;
     flex: 1;
+    justify-content: center;
 }
 
 #pch-fullPageMedia .leaflet-container {

+ 1 - 1
static/public/js/filters.js

@@ -73,7 +73,7 @@ window.FilterManager = (() => {
                         continue;
                     return false;
                 }
-                if (this.#filters[i].indexOf(mediaItem.meta[i]?.value) === -1)
+                if (this.#filters[i].indexOf(""+mediaItem.meta[i]?.value) === -1)
                     return false;
             }
             return true;

+ 2 - 1
static/public/js/uiFilter.js

@@ -86,7 +86,8 @@ $(() => {
 
         let container = document.getElementById('pch-filterbar');
         container.textContent = '';
-        for (let i of Object.keys(mediaManager.allMetaTypes).filter(i => mediaManager.allMetaTypes[i].type === 'string')) {
+        for (let i of Object.keys(mediaManager.allMetaTypes).filter(i => mediaManager.allMetaTypes[i].type === 'string' ||
+                ['orientation'].indexOf(i) >= 0)) {
             let filterUi = buildFilterBar(i, mediaManager.allMetaTypes[i].canBeEmpty, Array.from(mediaManager.allMeta[i]).sort());
             container.appendChild(filterUi.content);
         }