fix: 画像ファイルの縦横サイズの取得で Exif Orientation を考慮する (#8014)
* 画像ファイルの縦横サイズの取得で Exif Orientation を考慮する * test: Add rotate.jpg test * Webpublic 画像を返す時のみ Exif Orientation を考慮して縦横サイズを返す * test: Support orientation
This commit is contained in:
		
							parent
							
								
									f33ded3107
								
							
						
					
					
						commit
						22464c434e
					
				
					 7 changed files with 70 additions and 8 deletions
				
			
		|  | @ -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); | ||||||
|  |  | ||||||
|  | @ -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') | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
|  |  | ||||||
|  | @ -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; | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -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, | ||||||
| 		}); | 		}); | ||||||
| 	})); | 	})); | ||||||
| }); | }); | ||||||
|  |  | ||||||
							
								
								
									
										
											BIN
										
									
								
								packages/backend/test/resources/rotate.jpg
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								packages/backend/test/resources/rotate.jpg
									
										
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 12 KiB | 
|  | @ -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; | ||||||
| 			}); | 			}); | ||||||
|  |  | ||||||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue