diff --git a/packages/backend/src/misc/get-file-info.ts b/packages/backend/src/misc/get-file-info.ts index 39ba54139..8d7f6b1bf 100644 --- a/packages/backend/src/misc/get-file-info.ts +++ b/packages/backend/src/misc/get-file-info.ts @@ -19,6 +19,7 @@ export type FileInfo = { }; width?: number; height?: number; + orientation?: number; blurhash?: string; warnings: string[]; }; @@ -47,6 +48,7 @@ export async function getFileInfo(path: string): Promise { // image dimensions let width: 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)) { const imageSize = await detectImageSize(path).catch(e => { @@ -61,6 +63,7 @@ export async function getFileInfo(path: string): Promise { } else if (imageSize.wUnits === 'px') { width = imageSize.width; height = imageSize.height; + orientation = imageSize.orientation; // 制限を超えている画像は octet-stream にする if (imageSize.width > 16383 || imageSize.height > 16383) { @@ -87,6 +90,7 @@ export async function getFileInfo(path: string): Promise { type, width, height, + orientation, blurhash, warnings, }; @@ -163,6 +167,7 @@ async function detectImageSize(path: string): Promise<{ height: number; wUnits: string; hUnits: string; + orientation?: number; }> { const readable = fs.createReadStream(path); const imageSize = await probeImageSize(readable); diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 698dfac22..4ec7b94ed 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -77,7 +77,7 @@ export class DriveFile { default: {}, 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() @Column('boolean') diff --git a/packages/backend/src/models/repositories/drive-file.ts b/packages/backend/src/models/repositories/drive-file.ts index ddf9a46af..f2f0308dc 100644 --- a/packages/backend/src/models/repositories/drive-file.ts +++ b/packages/backend/src/models/repositories/drive-file.ts @@ -28,6 +28,19 @@ export class DriveFileRepository extends Repository { ); } + 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 { // リモートかつメディアプロキシ if (file.uri != null && file.userHost != null && config.mediaProxy != null) { @@ -122,7 +135,7 @@ export class DriveFileRepository extends Repository { size: file.size, isSensitive: file.isSensitive, blurhash: file.blurhash, - properties: file.properties, + properties: opts.self ? file.properties : this.getPublicProperties(file), url: opts.self ? file.url : this.getPublicUrl(file, false, meta), thumbnailUrl: this.getPublicUrl(file, true, meta), comment: file.comment, @@ -202,6 +215,11 @@ export const packedDriveFileSchema = { optional: true as const, nullable: false as const, example: 720 }, + orientation: { + type: 'number' as const, + optional: true as const, nullable: false as const, + example: 8 + }, avgColor: { type: 'string' as const, optional: true as const, nullable: false as const, diff --git a/packages/backend/src/services/drive/add-file.ts b/packages/backend/src/services/drive/add-file.ts index 6c5fefd4a..a57f9cf06 100644 --- a/packages/backend/src/services/drive/add-file.ts +++ b/packages/backend/src/services/drive/add-file.ts @@ -372,12 +372,16 @@ export default async function( const properties: { width?: number; height?: number; + orientation?: number; } = {}; if (info.width) { properties['width'] = info.width; properties['height'] = info.height; } + if (info.orientation != null) { + properties['orientation'] = info.orientation; + } const profile = user ? await UserProfiles.findOne(user.id) : null; diff --git a/packages/backend/test/get-file-info.ts b/packages/backend/test/get-file-info.ts index cc9eefbfc..a0146bd81 100644 --- a/packages/backend/test/get-file-info.ts +++ b/packages/backend/test/get-file-info.ts @@ -17,6 +17,7 @@ describe('Get file info', () => { }, width: undefined, height: undefined, + orientation: undefined, }); })); @@ -34,6 +35,7 @@ describe('Get file info', () => { }, width: 512, height: 512, + orientation: undefined, }); })); @@ -51,6 +53,7 @@ describe('Get file info', () => { }, width: 256, height: 256, + orientation: undefined, }); })); @@ -68,6 +71,7 @@ describe('Get file info', () => { }, width: 256, height: 256, + orientation: undefined, }); })); @@ -85,6 +89,7 @@ describe('Get file info', () => { }, width: 256, height: 256, + orientation: undefined, }); })); @@ -102,6 +107,7 @@ describe('Get file info', () => { }, width: 256, height: 256, + orientation: undefined, }); })); @@ -120,6 +126,7 @@ describe('Get file info', () => { }, width: 256, height: 256, + orientation: undefined, }); })); @@ -137,6 +144,25 @@ describe('Get file info', () => { }, width: 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, }); })); }); diff --git a/packages/backend/test/resources/rotate.jpg b/packages/backend/test/resources/rotate.jpg new file mode 100644 index 000000000..477c2baf5 Binary files /dev/null and b/packages/backend/test/resources/rotate.jpg differ diff --git a/packages/client/src/components/media-list.vue b/packages/client/src/components/media-list.vue index 38f0f7066..344ad8784 100644 --- a/packages/client/src/components/media-list.vue +++ b/packages/client/src/components/media-list.vue @@ -44,12 +44,18 @@ export default defineComponent({ onMounted(() => { const lightbox = new PhotoSwipeLightbox({ - dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => ({ - src: media.url, - w: media.properties.width, - h: media.properties.height, - alt: media.name, - })), + dataSource: props.mediaList.filter(media => media.type.startsWith('image')).map(media => { + const item = { + src: media.url, + w: media.properties.width, + 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, children: '.image', thumbSelector: '.image', @@ -77,6 +83,9 @@ export default defineComponent({ itemData.src = file.url; itemData.w = Number(file.properties.width); 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.thumbCropped = true; });