fix: 画像ファイルの縦横サイズの取得で Exif Orientation を考慮する (#8014)

* 画像ファイルの縦横サイズの取得で Exif Orientation を考慮する

* test: Add rotate.jpg test

* Webpublic 画像を返す時のみ Exif Orientation を考慮して縦横サイズを返す

* test: Support orientation
This commit is contained in:
xianon 2021-12-03 11:19:28 +09:00 committed by GitHub
parent f33ded3107
commit 22464c434e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 70 additions and 8 deletions

View file

@ -19,6 +19,7 @@ export type FileInfo = {
}; };
width?: number; width?: number;
height?: number; height?: number;
orientation?: number;
blurhash?: string; blurhash?: string;
warnings: string[]; warnings: string[];
}; };
@ -47,6 +48,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
// image dimensions // image dimensions
let width: number | undefined; let width: number | undefined;
let height: number | undefined; let height: number | undefined;
let orientation: number | undefined;
if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) { if (['image/jpeg', 'image/gif', 'image/png', 'image/apng', 'image/webp', 'image/bmp', 'image/tiff', 'image/svg+xml', 'image/vnd.adobe.photoshop'].includes(type.mime)) {
const imageSize = await detectImageSize(path).catch(e => { const imageSize = await detectImageSize(path).catch(e => {
@ -61,6 +63,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
} else if (imageSize.wUnits === 'px') { } else if (imageSize.wUnits === 'px') {
width = imageSize.width; width = imageSize.width;
height = imageSize.height; height = imageSize.height;
orientation = imageSize.orientation;
// 制限を超えている画像は octet-stream にする // 制限を超えている画像は octet-stream にする
if (imageSize.width > 16383 || imageSize.height > 16383) { if (imageSize.width > 16383 || imageSize.height > 16383) {
@ -87,6 +90,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> {
type, type,
width, width,
height, height,
orientation,
blurhash, blurhash,
warnings, warnings,
}; };
@ -163,6 +167,7 @@ async function detectImageSize(path: string): Promise<{
height: number; height: number;
wUnits: string; wUnits: string;
hUnits: string; hUnits: string;
orientation?: number;
}> { }> {
const readable = fs.createReadStream(path); const readable = fs.createReadStream(path);
const imageSize = await probeImageSize(readable); const imageSize = await probeImageSize(readable);

View file

@ -77,7 +77,7 @@ export class DriveFile {
default: {}, default: {},
comment: 'The any properties of the DriveFile. For example, it includes image width/height.' comment: 'The any properties of the DriveFile. For example, it includes image width/height.'
}) })
public properties: { width?: number; height?: number; avgColor?: string }; public properties: { width?: number; height?: number; orientation?: number; avgColor?: string };
@Index() @Index()
@Column('boolean') @Column('boolean')

View file

@ -28,6 +28,19 @@ export class DriveFileRepository extends Repository<DriveFile> {
); );
} }
public getPublicProperties(file: DriveFile): DriveFile['properties'] {
if (file.properties.orientation != null) {
const properties = JSON.parse(JSON.stringify(file.properties));
if (file.properties.orientation >= 5) {
[properties.width, properties.height] = [properties.height, properties.width];
}
properties.orientation = undefined;
return properties;
}
return file.properties;
}
public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null { public getPublicUrl(file: DriveFile, thumbnail = false, meta?: Meta): string | null {
// リモートかつメディアプロキシ // リモートかつメディアプロキシ
if (file.uri != null && file.userHost != null && config.mediaProxy != null) { if (file.uri != null && file.userHost != null && config.mediaProxy != null) {
@ -122,7 +135,7 @@ export class DriveFileRepository extends Repository<DriveFile> {
size: file.size, size: file.size,
isSensitive: file.isSensitive, isSensitive: file.isSensitive,
blurhash: file.blurhash, blurhash: file.blurhash,
properties: file.properties, properties: opts.self ? file.properties : this.getPublicProperties(file),
url: opts.self ? file.url : this.getPublicUrl(file, false, meta), url: opts.self ? file.url : this.getPublicUrl(file, false, meta),
thumbnailUrl: this.getPublicUrl(file, true, meta), thumbnailUrl: this.getPublicUrl(file, true, meta),
comment: file.comment, comment: file.comment,
@ -202,6 +215,11 @@ export const packedDriveFileSchema = {
optional: true as const, nullable: false as const, optional: true as const, nullable: false as const,
example: 720 example: 720
}, },
orientation: {
type: 'number' as const,
optional: true as const, nullable: false as const,
example: 8
},
avgColor: { avgColor: {
type: 'string' as const, type: 'string' as const,
optional: true as const, nullable: false as const, optional: true as const, nullable: false as const,

View file

@ -372,12 +372,16 @@ export default async function(
const properties: { const properties: {
width?: number; width?: number;
height?: number; height?: number;
orientation?: number;
} = {}; } = {};
if (info.width) { if (info.width) {
properties['width'] = info.width; properties['width'] = info.width;
properties['height'] = info.height; properties['height'] = info.height;
} }
if (info.orientation != null) {
properties['orientation'] = info.orientation;
}
const profile = user ? await UserProfiles.findOne(user.id) : null; const profile = user ? await UserProfiles.findOne(user.id) : null;

View file

@ -17,6 +17,7 @@ describe('Get file info', () => {
}, },
width: undefined, width: undefined,
height: undefined, height: undefined,
orientation: undefined,
}); });
})); }));
@ -34,6 +35,7 @@ describe('Get file info', () => {
}, },
width: 512, width: 512,
height: 512, height: 512,
orientation: undefined,
}); });
})); }));
@ -51,6 +53,7 @@ describe('Get file info', () => {
}, },
width: 256, width: 256,
height: 256, height: 256,
orientation: undefined,
}); });
})); }));
@ -68,6 +71,7 @@ describe('Get file info', () => {
}, },
width: 256, width: 256,
height: 256, height: 256,
orientation: undefined,
}); });
})); }));
@ -85,6 +89,7 @@ describe('Get file info', () => {
}, },
width: 256, width: 256,
height: 256, height: 256,
orientation: undefined,
}); });
})); }));
@ -102,6 +107,7 @@ describe('Get file info', () => {
}, },
width: 256, width: 256,
height: 256, height: 256,
orientation: undefined,
}); });
})); }));
@ -120,6 +126,7 @@ describe('Get file info', () => {
}, },
width: 256, width: 256,
height: 256, height: 256,
orientation: undefined,
}); });
})); }));
@ -137,6 +144,25 @@ describe('Get file info', () => {
}, },
width: 25000, width: 25000,
height: 25000, height: 25000,
orientation: undefined,
});
}));
it('Rotate JPEG', async (async () => {
const path = `${__dirname}/resources/rotate.jpg`;
const info = await getFileInfo(path) as any;
delete info.warnings;
delete info.blurhash;
assert.deepStrictEqual(info, {
size: 12624,
md5: '68d5b2d8d1d1acbbce99203e3ec3857e',
type: {
mime: 'image/jpeg',
ext: 'jpg'
},
width: 512,
height: 256,
orientation: 8,
}); });
})); }));
}); });

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View file

@ -44,12 +44,18 @@ export default defineComponent({
onMounted(() => { onMounted(() => {
const lightbox = new PhotoSwipeLightbox({ const lightbox = new PhotoSwipeLightbox({
dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => ({ dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => {
src: media.url, const item = {
w: media.properties.width, src: media.url,
h: media.properties.height, w: media.properties.width,
alt: media.name, h: media.properties.height,
})), alt: media.name,
};
if (media.properties.orientation != null && media.properties.orientation >= 5) {
[item.w, item.h] = [item.h, item.w];
}
return item;
}),
gallery: gallery.value, gallery: gallery.value,
children: '.image', children: '.image',
thumbSelector: '.image', thumbSelector: '.image',
@ -77,6 +83,9 @@ export default defineComponent({
itemData.src = file.url; itemData.src = file.url;
itemData.w = Number(file.properties.width); itemData.w = Number(file.properties.width);
itemData.h = Number(file.properties.height); itemData.h = Number(file.properties.height);
if (file.properties.orientation != null && file.properties.orientation >= 5) {
[itemData.w, itemData.h] = [itemData.h, itemData.w];
}
itemData.msrc = file.thumbnailUrl; itemData.msrc = file.thumbnailUrl;
itemData.thumbCropped = true; itemData.thumbCropped = true;
}); });