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; | ||||
| 	height?: number; | ||||
| 	orientation?: number; | ||||
| 	blurhash?: string; | ||||
| 	warnings: string[]; | ||||
| }; | ||||
|  | @ -47,6 +48,7 @@ export async function getFileInfo(path: string): Promise<FileInfo> { | |||
| 	// 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<FileInfo> { | |||
| 		} 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<FileInfo> { | |||
| 		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); | ||||
|  |  | |||
|  | @ -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') | ||||
|  |  | |||
|  | @ -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 { | ||||
| 		// リモートかつメディアプロキシ
 | ||||
| 		if (file.uri != null && file.userHost != null && config.mediaProxy != null) { | ||||
|  | @ -122,7 +135,7 @@ export class DriveFileRepository extends Repository<DriveFile> { | |||
| 			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, | ||||
|  |  | |||
|  | @ -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; | ||||
| 
 | ||||
|  |  | |||
|  | @ -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, | ||||
| 		}); | ||||
| 	})); | ||||
| }); | ||||
|  |  | |||
							
								
								
									
										
											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(() => { | ||||
| 			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; | ||||
| 			}); | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue