Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for issue #794 (bugfix) #826

Merged
merged 4 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -34,3 +34,11 @@ test.*
*.sublime-project
*.sublime-workspace
.DS_Store
test/folder-reset.js
test/folder-reset.js.map
test/TestHelper.js
test/TestHelper.js.map
test/frontend/translation.spec.js
test/frontend/translation.spec.js.map
/coverage/
.nyc_output/
3 changes: 2 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ In general, I'm happy to merge PRs, but I recommend filling a ticket and ask fir
1. Download the source files
2. install dependencies `npm install`
3. Build client `npm run run-dev`
* This will build the client with english localization and will keep building if you change the source files
* This will build the client with english localization and will keep building if you change the source files.
* Note: This process does not exit, so you need another terminal to run the next step.
4. Build the backend `npm run build-backend`
* This runs `tsc` that transpiles `.ts` files to `.js` so node can run them.
* To rebuild on change run `tsc -w`
Expand Down
Binary file added demo/images/Chars_exiftool.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
117 changes: 65 additions & 52 deletions src/backend/model/fileaccess/MetadataLoader.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,22 @@
import {VideoMetadata} from '../../../common/entities/VideoDTO';
import {FaceRegion, PhotoMetadata} from '../../../common/entities/PhotoDTO';
import {SideCar} from '../../../common/entities/MediaDTO';
import {Config} from '../../../common/config/private/Config';
import {Logger} from '../../Logger';
import * as fs from 'fs';
import {imageSize} from 'image-size';
import { imageSize } from 'image-size';
import { Config } from '../../../common/config/private/Config';
import { SideCar } from '../../../common/entities/MediaDTO';
import { FaceRegion, PhotoMetadata } from '../../../common/entities/PhotoDTO';
import { VideoMetadata } from '../../../common/entities/VideoDTO';
import { Logger } from '../../Logger';
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import * as ExifReader from 'exifreader';
import {ExifParserFactory, OrientationTypes} from 'ts-exif-parser';
import {IptcParser} from 'ts-node-iptc';
import {FFmpegFactory} from '../FFmpegFactory';
import {FfprobeData} from 'fluent-ffmpeg';
import {Utils} from '../../../common/Utils';
import {ExtensionDecorator} from '../extension/ExtensionDecorator';
import * as exifr from 'exifr';
import * as path from 'path';
import { FfprobeData } from 'fluent-ffmpeg';
import { FileHandle } from 'fs/promises';
import * as util from 'node:util';
import {FileHandle} from 'fs/promises';
import * as path from 'path';
import { ExifParserFactory, OrientationTypes } from 'ts-exif-parser';
import { IptcParser } from 'ts-node-iptc';
import { Utils } from '../../../common/Utils';
import { FFmpegFactory } from '../FFmpegFactory';
import { ExtensionDecorator } from '../extension/ExtensionDecorator';

const LOG_TAG = '[MetadataLoader]';
const ffmpeg = FFmpegFactory.get();
Expand Down Expand Up @@ -358,33 +357,44 @@
}

try {
// TODO: clean up the three different exif readers,
// and keep the minimum amount only
const exif: ExifReader.Tags & ExifReader.XmpTags & ExifReader.IccTags = ExifReader.load(data);
if (exif.Rating) {
metadata.rating = parseInt(exif.Rating.value as string, 10) as 0 | 1 | 2 | 3 | 4 | 5;
const exifrOptions = {
tiff: true,
xmp: true,
icc: false,
jfif: false, //not needed and not supported for png
ihdr: true,
iptc: false, //exifr reads UTF8-encoded data wrongly
exif: true,
gps: true,
translateValues: false, //don't translate orientation from numbers to strings etc.
mergeOutput: false //don't merge output, because things like Microsoft Rating (percent) and xmp.rating will be merged
};

const exif = await exifr.parse(data, exifrOptions);
if (exif.xmp && exif.xmp.Rating) {
metadata.rating = exif.xmp.Rating;
if (metadata.rating < 0) {
metadata.rating = 0;
}
}
if (
exif.subject &&
exif.subject.value &&
exif.subject.value.length > 0
) {
if (exif.dc &&
exif.dc.subject &&
exif.dc.subject.length > 0) {
const subj = Array.isArray(exif.dc.subject) ? exif.dc.subject : [exif.dc.subject];
if (metadata.keywords === undefined) {
metadata.keywords = [];
metadata.keywords = [];
}
for (const kw of exif.subject.value as ExifReader.XmpTag[]) {
if (metadata.keywords.indexOf(kw.description) === -1) {
metadata.keywords.push(kw.description);
}
for (const kw of subj) {
if (metadata.keywords.indexOf(kw) === -1) {
metadata.keywords.push(kw);
}
}
}
}
let orientation = OrientationTypes.TOP_LEFT;
if (exif.Orientation) {
if (exif.ifd0 &&
exif.ifd0.Orientation) {
orientation = parseInt(
exif.Orientation.value as any,
exif.ifd0.Orientation as any,

Check warning on line 397 in src/backend/model/fileaccess/MetadataLoader.ts

View workflow job for this annotation

GitHub Actions / test (18.x)

Unexpected any. Specify a different type
10
) as number;
}
Expand All @@ -396,9 +406,11 @@
metadata.size.height = height;
}

if (Config.Faces.enabled) {
if (Config.Faces.enabled &&
exif["mwg-rs"] &&
exif["mwg-rs"].Regions) {
const faces: FaceRegion[] = [];
const regionListVal = ((exif.Regions?.value as any)?.RegionList)?.value;
const regionListVal = Array.isArray(exif["mwg-rs"].Regions.RegionList) ? exif["mwg-rs"].Regions.RegionList : [exif["mwg-rs"].Regions.RegionList];
if (regionListVal) {
for (const regionRoot of regionListVal) {
let type;
Expand Down Expand Up @@ -442,16 +454,16 @@

/* Adobe Lightroom based face region structure */
if (
regionRoot.value &&
regionRoot.value['rdf:Description'] &&
regionRoot.value['rdf:Description'].value &&
regionRoot.value['rdf:Description'].value['mwg-rs:Area']
regionRoot &&
regionRoot['rdf:Description'] &&
regionRoot['rdf:Description'] &&
regionRoot['rdf:Description']['mwg-rs:Area']
) {
const region = regionRoot.value['rdf:Description'];
const regionBox = region.value['mwg-rs:Area'].attributes;
const region = regionRoot['rdf:Description'];
const regionBox = region['mwg-rs:Area'].attributes;

name = region.attributes['mwg-rs:Name'];
type = region.attributes['mwg-rs:Type'];
name = region['mwg-rs:Name'];
type = region['mwg-rs:Type'];
box = createFaceBox(
regionBox['stArea:w'],
regionBox['stArea:h'],
Expand All @@ -460,18 +472,19 @@
);
/* Load exiftool edited face region structure, see github issue #191 */
} else if (
regionRoot.Area &&
regionRoot &&
regionRoot.Name &&
regionRoot.Type
regionRoot.Type &&
regionRoot.Area
) {
const regionBox = regionRoot.Area.value;
name = regionRoot.Name.value;
type = regionRoot.Type.value;
const regionBox = regionRoot.Area;
name = regionRoot.Name;
type = regionRoot.Type;
box = createFaceBox(
regionBox.w.value,
regionBox.h.value,
regionBox.x.value,
regionBox.y.value
regionBox.w,
regionBox.h,
regionBox.x,
regionBox.y
);
}

Expand Down
Binary file added test/backend/assets/Chars.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions test/backend/assets/Chars.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"size": {
"width": 1920,
"height": 1080
},
"creationDate": 1706659327000,
"fileSize": 111432,
"positionData": {
"GPSData": {
"longitude": 14.162922,
"latitude": 57.780696
},
"country": "Sverige",
"state": "Jönköping",
"city": "Jönköping"
},
"keywords": [
],
"rating": 0,
"faces": [
{
"box": {
"width": 206,
"height": 257,
"left": 566,
"top": 144
},
"name": "æÆøØåÅéÉüÜäÄöÖïÏñÑ"
},
{
"name": "abcdefghijklmnopqrstuvwxyz",
"box": {
"width": 212,
"height": 265,
"left": 866,
"top": 144
}
},
{
"name": "abcdefghijklmnopqrstuvwxyz",
"box": {
"width": 212,
"height": 265,
"left": 1162,
"top": 150
}
}
]
}
Binary file added test/backend/assets/Chars_exiftool.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
49 changes: 49 additions & 0 deletions test/backend/assets/Chars_exiftool.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
{
"size": {
"width": 1920,
"height": 1080
},
"creationDate": 1706616000000,
"fileSize": 111050,
"positionData": {
"GPSData": {
"longitude": 14.162922,
"latitude": 57.780696
},
"country": "Sverige",
"state": "Jönköping",
"city": "Jönköping"
},
"keywords": [
],
"rating": 0,
"faces": [
{
"box": {
"width": 206,
"height": 257,
"left": 566,
"top": 144
},
"name": "æÆøØåÅéÉüÜäÄöÖïÏñÑ"
},
{
"name": "abcdefghijklmnopqrstuvwxyz",
"box": {
"width": 212,
"height": 265,
"left": 866,
"top": 144
}
},
{
"name": "abcdefghijklmnopqrstuvwxyz",
"box": {
"width": 212,
"height": 265,
"left": 1162,
"top": 150
}
}
]
}
31 changes: 31 additions & 0 deletions test/backend/assets/png_with_keyword_and_dates.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@

{
"size": {
"width": 26,
"height": 26
},
"creationDate": 1707167247786,
"fileSize": 5758,
"keywords": [
],
"faces": [
{
"name": "raspberry",
"box": {
"width": 21,
"height": 18,
"left": 3,
"top": 8
}
},
{
"name": "leaf",
"box": {
"width": 9,
"height": 7,
"left": 14,
"top": 1
}
}
]
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
3 changes: 1 addition & 2 deletions test/backend/unit/model/threading/DiskManagerWorker.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,11 @@ describe('DiskMangerWorker', () => {
ProjectPath.ImageFolder = path.join(__dirname, '/../../../assets');
const dir = await DiskManager.scanDirectory('/');
// should match the number of media (photo/video) files in the assets folder
expect(dir.media.length).to.be.equals(11);
expect(dir.media.length).to.be.equals(14);
// eslint-disable-next-line @typescript-eslint/no-var-requires
const expected = require(path.join(__dirname, '/../../../assets/test image öüóőúéáű-.,.json'));
const i = dir.media.findIndex(m => m.name === 'test image öüóőúéáű-.,.jpg');
expect(Utils.clone(dir.media[i].name)).to.be.deep.equal('test image öüóőúéáű-.,.jpg');
expect(Utils.clone(dir.media[i].metadata)).to.be.deep.equal(expected);
});

});
10 changes: 10 additions & 0 deletions test/backend/unit/model/threading/MetaDataLoader.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,16 @@ describe('MetadataLoader', () => {
const expected = require(path.join(__dirname, '/../../../assets/old_photo.json'));
expect(Utils.clone(data)).to.be.deep.equal(expected);
});
it('should load jpg with special characters', async () => {
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/Chars.jpg'));
const expected = require(path.join(__dirname, '/../../../assets/Chars.json'));
expect(Utils.clone(data)).to.be.deep.equal(expected);
});
it('should load jpg with special characters saved by exiftool', async () => {
const data = await MetadataLoader.loadPhotoMetadata(path.join(__dirname, '/../../../assets/Chars_exiftool.jpg'));
const expected = require(path.join(__dirname, '/../../../assets/Chars_exiftool.json'));
expect(Utils.clone(data)).to.be.deep.equal(expected);
});

describe('should load jpg with proper height and orientation', () => {
it('jpg 1', async () => {
Expand Down
Loading