1
0
mirror of https://github.com/bpatrik/pigallery2.git synced 2025-01-12 04:23:09 +02:00

fixing exif based orientation error

This commit is contained in:
Patrik J. Braun 2018-11-02 10:40:09 +01:00
parent e084ec2d15
commit 92fca05b22
23 changed files with 233 additions and 89 deletions

View File

@ -139,6 +139,10 @@ apt-get install build-essential libkrb5-dev gcc g++
* you can write some note in the blog.md for every directory
* bug free :) - `In progress`
## Known errors
There is no nice way to handle EXIF orientation tag properly.
The page handles these photos, but might cause same error in the user experience (e.g.: the pages loads those photos slower. See issue #11)
## Credits
Crossbrowser testing sponsored by [Browser Stack](https://www.browserstack.com)

View File

@ -1,12 +1,5 @@
import * as winston from 'winston';
declare module 'winston' {
interface LoggerInstance {
logFileName: string;
logFilePath: string;
}
}
export const winstonSettings = {
transports: [
new winston.transports.Console({

View File

@ -1,4 +1,3 @@
///<reference path="exif.d.ts"/>
import {DirectoryDTO} from '../../common/entities/DirectoryDTO';
import {Logger} from '../Logger';
import {Config} from '../../common/config/private/Config';

View File

@ -1,32 +0,0 @@
declare module 'node-iptc' {
function e(data): any;
module e {
}
export = e;
}
declare module 'exif-parser' {
export interface ExifData {
tags: any;
imageSize: any;
}
export interface ExifObject {
enableTagNames(value: boolean);
enableImageSize(value: boolean);
enableReturnTags(value: boolean);
parse(): ExifData;
}
export function create(data: any): ExifObject;
}

View File

@ -1,7 +1,7 @@
import {Column, Entity, ManyToOne, PrimaryGeneratedColumn} from 'typeorm';
import {CameraMetadata, GPSMetadata, ImageSize, PhotoDTO, PhotoMetadata, PositionMetaData} from '../../../../common/entities/PhotoDTO';
import {DirectoryEntity} from './DirectoryEntity';
import {OrientationTypes} from 'ts-exif-parser';
@Entity()
export class CameraMetadataEntity implements CameraMetadata {
@ -80,6 +80,9 @@ export class PhotoMetadataEntity implements PhotoMetadata {
@Column(type => PositionMetaDataEntity)
positionData: PositionMetaDataEntity;
@Column('tinyint')
orientation: OrientationTypes;
@Column(type => ImageSizeEntity)
size: ImageSizeEntity;

View File

@ -4,7 +4,7 @@ import {DirectoryDTO} from '../../../common/entities/DirectoryDTO';
import {CameraMetadata, GPSMetadata, ImageSize, PhotoDTO, PhotoMetadata} from '../../../common/entities/PhotoDTO';
import {Logger} from '../../Logger';
import {IptcParser} from 'ts-node-iptc';
import {ExifParserFactory} from 'ts-exif-parser';
import {ExifParserFactory, OrientationTypes} from 'ts-exif-parser';
import {ProjectPath} from '../../ProjectPath';
import {Config} from '../../../common/config/private/Config';
@ -29,7 +29,6 @@ export class DiskMangerWorker {
public static scanDirectory(relativeDirectoryName: string, maxPhotos: number = null, photosOnly: boolean = false): Promise<DirectoryDTO> {
return new Promise<DirectoryDTO>((resolve, reject) => {
const directoryName = path.basename(relativeDirectoryName);
const directoryParent = path.join(path.dirname(relativeDirectoryName), path.sep);
const absoluteDirectoryName = path.join(ProjectPath.ImageFolder, relativeDirectoryName);
@ -94,10 +93,10 @@ export class DiskMangerWorker {
cameraData: {},
positionData: null,
size: {},
orientation: OrientationTypes.TOP_RIGHT,
creationDate: 0,
fileSize: 0
};
try {
try {
@ -130,6 +129,9 @@ export class DiskMangerWorker {
metadata.creationDate = exif.tags.CreateDate || exif.tags.DateTimeOriginal || exif.tags.ModifyDate;
}
if (exif.tags.Orientation) {
metadata.orientation = exif.tags.Orientation;
}
if (exif.imageSize) {
metadata.size = <ImageSize> {width: exif.imageSize.width, height: exif.imageSize.height};

View File

@ -8,7 +8,7 @@ export class Worker {
public static process() {
Logger.debug('Worker is waiting for tasks');
process.on('message', async (task: WorkerTask)=> {
process.on('message', async (task: WorkerTask) => {
try {
let result = null;
switch (task.type) {

View File

@ -1,4 +1,6 @@
import {DirectoryDTO} from './DirectoryDTO';
import {ImageSize} from './PhotoDTO';
import {OrientationTypes} from 'ts-exif-parser';
export interface PhotoDTO {
id: number;
@ -13,6 +15,7 @@ export interface PhotoMetadata {
keywords: Array<string>;
cameraData: CameraMetadata;
positionData: PositionMetaData;
orientation: OrientationTypes;
size: ImageSize;
creationDate: number;
fileSize: number;
@ -57,4 +60,25 @@ export module PhotoDTO {
photo.metadata.positionData.GPSData.latitude &&
photo.metadata.positionData.GPSData.longitude));
};
export const isSideWay = (photo: PhotoDTO): boolean => {
return photo.metadata.orientation === OrientationTypes.LEFT_TOP ||
photo.metadata.orientation === OrientationTypes.RIGHT_TOP ||
photo.metadata.orientation === OrientationTypes.LEFT_BOTTOM ||
photo.metadata.orientation === OrientationTypes.RIGHT_BOTTOM;
};
export const getRotatedSize = (photo: PhotoDTO): ImageSize => {
if (isSideWay(photo)) {
// noinspection JSSuspiciousNameCombination
return {width: photo.metadata.size.height, height: photo.metadata.size.width};
}
return photo.metadata.size;
};
export const calcRotatedAspectRatio = (photo: PhotoDTO): number => {
const size = getRotatedSize(photo);
return size.width / size.height;
};
}

View File

@ -72,6 +72,7 @@ import {IconizeSortingMethod} from './pipes/IconizeSortingMethod';
import {StringifySortingMethod} from './pipes/StringifySortingMethod';
import {RandomQueryBuilderGalleryComponent} from './gallery/random-query-builder/random-query-builder.gallery.component';
import {RandomPhotoSettingsComponent} from './settings/random-photo/random-photo.settings.component';
import {FixOrientationPipe} from './gallery/FixOrientationPipe';
@Injectable()
export class GoogleMapsConfig {
@ -166,7 +167,9 @@ export function translationsFactory(locale: string) {
IndexingSettingsComponent,
StringifyRole,
IconizeSortingMethod,
StringifySortingMethod],
StringifySortingMethod,
FixOrientationPipe
],
providers: [
{provide: UrlSerializer, useClass: CustomUrlSerializer},
{provide: LAZY_MAPS_API_CONFIG, useClass: GoogleMapsConfig},

View File

@ -0,0 +1,82 @@
import {Pipe, PipeTransform} from '@angular/core';
import {OrientationTypes} from 'ts-exif-parser';
/**
* This pipe is used to fix thumbnail and photo orientation based on their exif orientation tag
*/
@Pipe({name: 'fixOrientation'})
export class FixOrientationPipe implements PipeTransform {
public static transform(imageSrc: string, orientation: OrientationTypes): Promise<string> {
if (orientation === OrientationTypes.TOP_LEFT) {
return Promise.resolve(imageSrc);
}
return new Promise((resolve) => {
const img = new Image();
// noinspection SpellCheckingInspection
img.onload = () => {
const width = img.width,
height = img.height,
canvas = document.createElement('canvas'),
ctx = canvas.getContext('2d');
// set proper canvas dimensions before transform & export
if (OrientationTypes.BOTTOM_LEFT < orientation &&
orientation < OrientationTypes.LEFT_BOTTOM) {
// noinspection JSSuspiciousNameCombination
canvas.width = height;
// noinspection JSSuspiciousNameCombination
canvas.height = width;
} else {
canvas.width = width;
canvas.height = height;
}
// transform context before drawing image
switch (orientation) {
case OrientationTypes.TOP_RIGHT: // 2
ctx.transform(-1, 0, 0, 1, width, 0);
break;
case OrientationTypes.BOTTOM_RIGHT: // 3
ctx.transform(-1, 0, 0, -1, width, height);
break;
case OrientationTypes.BOTTOM_LEFT: // 4
ctx.transform(1, 0, 0, -1, 0, height);
break;
case OrientationTypes.LEFT_TOP: // 5
ctx.transform(0, 1, 1, 0, 0, 0);
break;
case OrientationTypes.RIGHT_TOP: // 6
ctx.transform(0, 1, -1, 0, height, 0);
break;
case OrientationTypes.RIGHT_BOTTOM: // 7
ctx.transform(0, -1, -1, 0, height, width);
break;
case OrientationTypes.LEFT_BOTTOM: // 8
ctx.transform(0, -1, 1, 0, 0, width);
break;
default:
break;
}
// draw image
ctx.drawImage(img, 0, 0);
// export base64
resolve(canvas.toDataURL());
};
img.onerror = () => {
resolve(imageSrc);
};
img.src = imageSrc;
});
}
transform(imageSrc: string, orientation: OrientationTypes): Promise<string> {
return FixOrientationPipe.transform(imageSrc, orientation);
}
}

View File

@ -54,3 +54,28 @@ a:hover .photo-container {
width: 180px;
white-space: normal;
}
/* transforming photo, based on exif orientation*/
.photo-orientation-1 {
}
.photo-orientation-2 {
transform: rotateY(180deg);
}
.photo-orientation-3 {
transform: rotate(180deg);
}
.photo-orientation-4 {
transform: rotate(180deg) rotateY(180deg);
}
.photo-orientation-5 {
transform: rotate(270deg) rotateY(180deg);
}
.photo-orientation-6 {
transform: rotate(90deg);
}
.photo-orientation-7 {
transform: rotate(90deg) rotateY(180deg);
}
.photo-orientation-8 {
transform: rotate(270deg);
}

View File

@ -7,7 +7,8 @@
<div class="photo-container"
[style.width.px]="calcSize()"
[style.height.px]="calcSize()">
<div class="photo" *ngIf="thumbnail && thumbnail.Available"
<div [class]="'photo ' + (SamplePhoto ? 'photo-orientation-'+SamplePhoto.metadata.orientation : '')"
*ngIf="thumbnail && thumbnail.Available"
[style.background-image]="getSanitizedThUrl()"></div>
<span *ngIf="!thumbnail || !thumbnail.Available" class="oi oi-folder no-image"

View File

@ -5,9 +5,9 @@ import {RouterLink} from '@angular/router';
import {Utils} from '../../../../common/Utils';
import {Photo} from '../Photo';
import {Thumbnail, ThumbnailManagerService} from '../thumnailManager.service';
import {ShareService} from '../share.service';
import {PageHelper} from '../../model/page.helper';
import {QueryService} from '../../model/query.service';
import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
@Component({
selector: 'app-gallery-directory',
@ -28,6 +28,13 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy {
size: number = null;
public get SamplePhoto(): PhotoDTO {
if (this.directory.photos.length > 0) {
return this.directory.photos[0];
}
return null;
}
getSanitizedThUrl() {
return this._sanitizer.bypassSecurityTrustStyle('url(' + encodeURI(this.thumbnail.Src).replace(/\(/g, '%28')
.replace(/\)/g, '%29') + ')');
@ -51,7 +58,7 @@ export class GalleryDirectoryComponent implements OnInit, OnDestroy {
ngOnInit() {
if (this.directory.photos.length > 0) {
this.thumbnail = this.thumbnailService.getThumbnail(new Photo(this.directory.photos[0], this.calcSize(), this.calcSize()));
this.thumbnail = this.thumbnailService.getThumbnail(new Photo(this.SamplePhoto, this.calcSize(), this.calcSize()));
}
}

View File

@ -2,12 +2,12 @@ import {PhotoDTO} from '../../../../common/entities/PhotoDTO';
export class GridRowBuilder {
private photoRow: Array<PhotoDTO> = [];
private photoRow: PhotoDTO[] = [];
private photoIndex = 0; // index of the last pushed photo to the photoRow
constructor(private photos: Array<PhotoDTO>,
constructor(private photos: PhotoDTO[],
private startIndex: number,
private photoMargin: number,
private containerWidth: number) {
@ -41,7 +41,7 @@ export class GridRowBuilder {
return true;
}
public getPhotoRow(): Array<PhotoDTO> {
public getPhotoRow(): PhotoDTO[] {
return this.photoRow;
}
@ -61,7 +61,8 @@ export class GridRowBuilder {
public calcRowHeight(): number {
let width = 0;
for (let i = 0; i < this.photoRow.length; i++) {
width += ((this.photoRow[i].metadata.size.width) / (this.photoRow[i].metadata.size.height)); // summing up aspect ratios
const size = PhotoDTO.getRotatedSize(this.photoRow[i]);
width += (size.width / size.height); // summing up aspect ratios
}
const height = (this.containerWidth - this.photoRow.length * (this.photoMargin * 2) - 1) / width; // cant be equal -> width-1

View File

@ -189,7 +189,7 @@ export class GalleryGridComponent implements OnChanges, OnInit, AfterViewInit, O
const imageHeight = rowHeight - (this.IMAGE_MARGIN * 2);
photoRowBuilder.getPhotoRow().forEach((photo) => {
const imageWidth = imageHeight * (photo.metadata.size.width / photo.metadata.size.height);
const imageWidth = imageHeight * PhotoDTO.calcRotatedAspectRatio(photo);
this.photosToRender.push(new GridPhoto(photo, imageWidth, imageHeight, this.renderedPhotoIndex));
});

View File

@ -1,5 +1,6 @@
<div #photoContainer class="photo-container" (mouseover)="mouseOver()" (mouseout)="mouseOut()">
<img #img [src]="thumbnail.Src" *ngIf="thumbnail.Available">
<img #img [src]="thumbnail.Src | fixOrientation:gridPhoto.photo.metadata.orientation | async"
*ngIf="thumbnail.Available">
<app-gallery-grid-photo-loading
[error]="thumbnail.Error"
@ -31,7 +32,7 @@
<a *ngIf="searchEnabled"
[routerLink]="['/search', keyword, {type: SearchTypes[SearchTypes.keyword]}]">#{{keyword}}</a>
<span *ngIf="!searchEnabled">#{{keyword}}</span>
<ng-template [ngIf]="!last">, </ng-template>
<ng-template [ngIf]="!last">,</ng-template>
</ng-template>
</div>

View File

@ -2,12 +2,12 @@
<img *ngIf="showThumbnail()"
[style.width.%]="imageSize.width"
[style.height.%]="imageSize.height"
[src]="thumbnailPath()"/>
[src]="thumbnailSrc"/>
<img *ngIf="gridPhoto !== null && loadImage"
<img *ngIf="gridPhoto !== null && loadImage && photoSrc"
[style.width.%]="imageSize.width"
[style.height.%]="imageSize.height"
[src]="gridPhoto.getPhotoPath()"
[src]="photoSrc"
(load)="onImageLoad()"
(error)="onImageError()"/>

View File

@ -1,5 +1,7 @@
import {Component, ElementRef, Input, OnChanges} from '@angular/core';
import {GridPhoto} from '../../grid/GridPhoto';
import {PhotoDTO} from '../../../../../common/entities/PhotoDTO';
import {FixOrientationPipe} from '../../FixOrientationPipe';
@Component({
selector: 'app-gallery-lightbox-photo',
@ -11,26 +13,43 @@ export class GalleryLightboxPhotoComponent implements OnChanges {
@Input() gridPhoto: GridPhoto;
@Input() loadImage = false;
@Input() windowAspect = 1;
prevGirdPhoto = null;
public imageSize = {width: 'auto', height: '100'};
imageLoaded = false;
private imageLoaded = false;
public imageLoadFinished = false;
thumbnailSrc: string = null;
photoSrc: string = null;
constructor(public elementRef: ElementRef) {
}
ngOnChanges() {
this.imageLoaded = false;
this.imageLoadFinished = false;
this.setImageSize();
if (this.prevGirdPhoto !== this.gridPhoto) {
this.prevGirdPhoto = this.gridPhoto;
this.thumbnailSrc = null;
this.photoSrc = null;
}
if (this.thumbnailSrc == null && this.gridPhoto && this.ThumbnailUrl !== null) {
FixOrientationPipe.transform(this.ThumbnailUrl, this.gridPhoto.photo.metadata.orientation)
.then((src) => this.thumbnailSrc = src);
}
if (this.photoSrc == null && this.gridPhoto && this.loadImage) {
FixOrientationPipe.transform(this.gridPhoto.getPhotoPath(), this.gridPhoto.photo.metadata.orientation)
.then((src) => this.thumbnailSrc = src);
}
}
onImageError() {
// TODO:handle error
this.imageLoadFinished = true;
console.error('cant load image');
console.error('Error: cannot load image for lightbox url: ' + this.gridPhoto.getPhotoPath());
}
@ -39,7 +58,7 @@ export class GalleryLightboxPhotoComponent implements OnChanges {
this.imageLoaded = true;
}
public thumbnailPath(): string {
private get ThumbnailUrl(): string {
if (this.gridPhoto.isThumbnailAvailable() === true) {
return this.gridPhoto.getThumbnailPath();
}
@ -50,8 +69,14 @@ export class GalleryLightboxPhotoComponent implements OnChanges {
return null;
}
public get PhotoSrc(): string {
return this.gridPhoto.getPhotoPath();
}
public showThumbnail(): boolean {
return this.gridPhoto && !this.imageLoaded &&
return this.gridPhoto &&
!this.imageLoaded &&
this.thumbnailSrc !== null &&
(this.gridPhoto.isThumbnailAvailable() || this.gridPhoto.isReplacementThumbnailAvailable());
}
@ -61,7 +86,7 @@ export class GalleryLightboxPhotoComponent implements OnChanges {
}
const photoAspect = this.gridPhoto.photo.metadata.size.width / this.gridPhoto.photo.metadata.size.height;
const photoAspect = PhotoDTO.calcRotatedAspectRatio(this.gridPhoto.photo);
if (photoAspect < this.windowAspect) {
this.imageSize.height = '100';

View File

@ -14,14 +14,14 @@
*ngFor="let photo of mapPhotos"
[latitude]="photo.latitude"
[longitude]="photo.longitude"
[iconUrl]="photo.iconUrl"
[iconUrl]="photo.iconUrl | fixOrientation:photo.orientation | async"
(markerClick)="loadPreview(photo)"
[agmFitBounds]="true">
<agm-info-window>
<img *ngIf="photo.preview.thumbnail.Src"
[style.width.px]="photo.preview.width"
[style.height.px]="photo.preview.height"
[src]="photo.preview.thumbnail.Src">
[src]="photo.preview.thumbnail.Src | fixOrientation:photo.orientation | async">
<div class="preview-loading"
[style.width.px]="photo.preview.width"
[style.height.px]="photo.preview.height"

View File

@ -7,6 +7,7 @@ import {IconThumbnail, Thumbnail, ThumbnailManagerService} from '../../thumnailM
import {IconPhoto} from '../../IconPhoto';
import {Photo} from '../../Photo';
import {PageHelper} from '../../../model/page.helper';
import {OrientationTypes} from 'ts-exif-parser';
@Component({
@ -118,6 +119,7 @@ export class GalleryMapLightboxComponent implements OnChanges, AfterViewInit {
latitude: p.metadata.positionData.GPSData.latitude,
longitude: p.metadata.positionData.GPSData.longitude,
iconThumbnail: iconTh,
orientation: p.metadata.orientation,
preview: {
width: width,
height: height,
@ -183,6 +185,7 @@ export interface MapPhoto {
longitude: number;
iconUrl?: string;
iconThumbnail: IconThumbnail;
orientation: OrientationTypes;
preview: {
width: number;
height: number;

View File

@ -11,3 +11,6 @@
padding-bottom: 1px;
}
#shareButton span{
padding-right: 0.3rem;
}

View File

@ -30,7 +30,7 @@ export abstract class ThumbnailBase {
protected available = false;
protected src: string = null;
protected loading = false;
public loading = false;
protected error = false;
protected onLoad: Function = null;
protected thumbnailTask: ThumbnailTaskEntity = null;

View File

@ -34,11 +34,11 @@
"cookie-session": "2.0.0-beta.3",
"ejs": "2.6.1",
"express": "4.16.4",
"jimp": "0.5.4",
"jimp": "0.5.6",
"locale": "0.1.0",
"reflect-metadata": "0.1.12",
"sqlite3": "4.0.2",
"ts-exif-parser": "0.1.24",
"sqlite3": "4.0.3",
"ts-exif-parser": "0.1.3",
"ts-node-iptc": "1.0.10",
"typeconfig": "1.0.6",
"typeorm": "0.2.8",
@ -46,20 +46,20 @@
},
"devDependencies": {
"@agm/core": "1.0.0-beta.5",
"@angular-devkit/build-angular": "0.10.2",
"@angular-devkit/build-optimizer": "0.10.2",
"@angular/animations": "7.0.0",
"@angular/cli": "7.0.2",
"@angular/common": "7.0.0",
"@angular/compiler": "7.0.0",
"@angular/compiler-cli": "7.0.0",
"@angular/core": "7.0.0",
"@angular/forms": "7.0.0",
"@angular/http": "7.0.0",
"@angular/language-service": "7.0.0",
"@angular/platform-browser": "7.0.0",
"@angular/platform-browser-dynamic": "7.0.0",
"@angular/router": "7.0.0",
"@angular-devkit/build-angular": "0.10.3",
"@angular-devkit/build-optimizer": "0.10.3",
"@angular/animations": "7.0.1",
"@angular/cli": "7.0.3",
"@angular/common": "7.0.1",
"@angular/compiler": "7.0.1",
"@angular/compiler-cli": "7.0.1",
"@angular/core": "7.0.1",
"@angular/forms": "7.0.1",
"@angular/http": "7.0.1",
"@angular/language-service": "7.0.1",
"@angular/platform-browser": "7.0.1",
"@angular/platform-browser-dynamic": "7.0.1",
"@angular/router": "7.0.1",
"@ngx-translate/i18n-polyfill": "1.0.0",
"@types/bcryptjs": "2.4.2",
"@types/chai": "4.1.6",
@ -69,7 +69,7 @@
"@types/jasmine": "2.8.9",
"@types/node": "10.12.0",
"@types/sharp": "0.21.0",
"@types/winston": "2.3.9",
"@types/winston": "2.4.4",
"bootstrap": "4.1.3",
"chai": "4.2.0",
"codelyzer": "4.5.0",
@ -81,10 +81,10 @@
"gulp-zip": "4.2.0",
"hammerjs": "2.0.8",
"intl": "1.2.5",
"jasmine-core": "3.2.1",
"jasmine-core": "3.3.0",
"jasmine-spec-reporter": "4.2.1",
"jw-bootstrap-switch-ng2": "2.0.2",
"karma": "3.0.0",
"karma": "3.1.1",
"karma-chrome-launcher": "2.2.0",
"karma-cli": "1.0.1",
"karma-coverage-istanbul-reporter": "2.0.4",
@ -123,6 +123,6 @@
"sharp": "0.21.0"
},
"engines": {
"node": ">= 6.9 <=10.0"
"node": ">= 6.9 <11.0"
}
}