mirror of
https://github.com/Afilmory/afilmory
synced 2026-02-01 22:48:17 +00:00
@@ -49,7 +49,6 @@ export const ExifPanel: FC<{
|
||||
const imageFormat = getImageFormat(
|
||||
currentPhoto.originalUrl || currentPhoto.s3Key || '',
|
||||
)
|
||||
const totalPixels = (currentPhoto.height * currentPhoto.width) / 1000000
|
||||
|
||||
return (
|
||||
<m.div
|
||||
@@ -115,10 +114,12 @@ export const ExifPanel: FC<{
|
||||
label={t('exif.file.size')}
|
||||
value={`${(currentPhoto.size / 1024 / 1024).toFixed(1)}MB`}
|
||||
/>
|
||||
{totalPixels && (
|
||||
{formattedExifData?.megaPixels && (
|
||||
<Row
|
||||
label={t('exif.pixels')}
|
||||
value={`${Math.floor(totalPixels)} MP`}
|
||||
value={`${Math.floor(
|
||||
Number.parseFloat(formattedExifData.megaPixels),
|
||||
)} MP`}
|
||||
/>
|
||||
)}
|
||||
{formattedExifData?.colorSpace && (
|
||||
|
||||
@@ -10,16 +10,8 @@ import TextureWorkerRaw from './texture.worker?raw'
|
||||
|
||||
// 瓦片系统配置
|
||||
const TILE_SIZE = 512 // 每个瓦片的像素大小
|
||||
// 平台调优:iOS 设备上降低并发和缓存以减少内存峰值
|
||||
function isIOSPlatform(): boolean {
|
||||
if (typeof navigator === 'undefined') return false
|
||||
const ua = navigator.userAgent || ''
|
||||
return /iP(?:ad|hone|od)/.test(ua)
|
||||
}
|
||||
|
||||
// 作为实例字段使用的默认阈值(避免在SSR时访问window)
|
||||
const DEFAULT_MAX_TILES_PER_FRAME = 4
|
||||
const DEFAULT_TILE_CACHE_SIZE = 32
|
||||
const MAX_TILES_PER_FRAME = 4 // 每帧最多创建的瓦片数量
|
||||
const TILE_CACHE_SIZE = 32 // 最大缓存瓦片数量
|
||||
|
||||
// 瓦片信息接口
|
||||
interface TileInfo {
|
||||
@@ -87,7 +79,6 @@ export class WebGLImageViewerEngine extends ImageViewerEngineBase {
|
||||
private targetTranslateX = 0
|
||||
private targetTranslateY = 0
|
||||
private animationStartLOD = -1
|
||||
// 动画焦点(已回滚到默认行为,无额外 pivot 锁定状态)
|
||||
|
||||
// 简化的纹理管理
|
||||
private currentLOD = 1 // 默认使用正常质量
|
||||
@@ -110,10 +101,6 @@ export class WebGLImageViewerEngine extends ImageViewerEngineBase {
|
||||
private worker: Worker | null = null
|
||||
private textureWorkerInitialized = false
|
||||
|
||||
// 平台相关的瓦片参数
|
||||
private MAX_TILES_PER_FRAME = DEFAULT_MAX_TILES_PER_FRAME
|
||||
private TILE_CACHE_SIZE = DEFAULT_TILE_CACHE_SIZE
|
||||
|
||||
// 事件处理器绑定
|
||||
private boundHandleMouseDown: (e: MouseEvent) => void
|
||||
private boundHandleMouseMove: (e: MouseEvent) => void
|
||||
@@ -154,8 +141,7 @@ export class WebGLImageViewerEngine extends ImageViewerEngineBase {
|
||||
// 初始化 WebGL
|
||||
const gl = canvas.getContext('webgl', {
|
||||
alpha: true,
|
||||
// 使用预乘 alpha,避免在与页面合成时出现 1px 边缘伪影
|
||||
premultipliedAlpha: true,
|
||||
premultipliedAlpha: false,
|
||||
antialias: true,
|
||||
powerPreference: 'default',
|
||||
})
|
||||
@@ -180,12 +166,6 @@ export class WebGLImageViewerEngine extends ImageViewerEngineBase {
|
||||
this.initWorker()
|
||||
this.setupEventListeners()
|
||||
|
||||
// 初始化平台相关阈值
|
||||
if (isIOSPlatform()) {
|
||||
this.MAX_TILES_PER_FRAME = 2
|
||||
this.TILE_CACHE_SIZE = 16
|
||||
}
|
||||
|
||||
this.isLoadingTexture = false
|
||||
this.notifyLoadingStateChange(false)
|
||||
}
|
||||
@@ -207,9 +187,7 @@ export class WebGLImageViewerEngine extends ImageViewerEngineBase {
|
||||
|
||||
private resizeCanvas() {
|
||||
const rect = this.canvas.getBoundingClientRect()
|
||||
// 限制 iOS 上的 DPR 以减少渲染分辨率和显存压力
|
||||
const dpr = window.devicePixelRatio || 1
|
||||
this.devicePixelRatio = isIOSPlatform() ? Math.min(dpr, 2) : dpr
|
||||
this.devicePixelRatio = window.devicePixelRatio || 1
|
||||
|
||||
this.canvasWidth = rect.width
|
||||
this.canvasHeight = rect.height
|
||||
@@ -257,24 +235,9 @@ export class WebGLImageViewerEngine extends ImageViewerEngineBase {
|
||||
|
||||
gl.useProgram(this.program)
|
||||
|
||||
// 启用混合(针对预乘 alpha 使用正确的混合函数)
|
||||
// 启用混合
|
||||
gl.enable(gl.BLEND)
|
||||
gl.blendFuncSeparate(
|
||||
gl.ONE,
|
||||
gl.ONE_MINUS_SRC_ALPHA,
|
||||
gl.ONE,
|
||||
gl.ONE_MINUS_SRC_ALPHA,
|
||||
)
|
||||
|
||||
// 上传像素时的解包设置,避免多余的颜色空间转换和预乘
|
||||
gl.pixelStorei(gl.UNPACK_FLIP_Y_WEBGL, 0)
|
||||
gl.pixelStorei(gl.UNPACK_PREMULTIPLY_ALPHA_WEBGL, 0)
|
||||
// 避免色彩空间转换导致的额外开销
|
||||
// @ts-ignore - 常量属于扩展
|
||||
if (gl.UNPACK_COLORSPACE_CONVERSION_WEBGL !== undefined) {
|
||||
// @ts-ignore
|
||||
gl.pixelStorei(gl.UNPACK_COLORSPACE_CONVERSION_WEBGL, gl.NONE)
|
||||
}
|
||||
gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA)
|
||||
|
||||
// 创建几何体
|
||||
const positions = new Float32Array([
|
||||
@@ -503,25 +466,20 @@ export class WebGLImageViewerEngine extends ImageViewerEngineBase {
|
||||
|
||||
// 寻找最佳的 LOD 级别
|
||||
// 我们希望找到一个 LOD 级别,它的缩放比例刚好大于或等于所需的缩放比例
|
||||
const maxLodIndex = isIOSPlatform()
|
||||
? SIMPLE_LOD_LEVELS.findIndex((l) => l.scale >= 1) // 限制 iOS 最高 LOD = 1x
|
||||
: SIMPLE_LOD_LEVELS.length - 1
|
||||
for (let i = 0; i <= SIMPLE_LOD_LEVELS.length - 1; i++) {
|
||||
const level = SIMPLE_LOD_LEVELS[i]
|
||||
if (level.scale >= requiredScale) {
|
||||
return Math.min(i, maxLodIndex)
|
||||
for (const [i, SIMPLE_LOD_LEVEL] of SIMPLE_LOD_LEVELS.entries()) {
|
||||
if (SIMPLE_LOD_LEVEL.scale >= requiredScale) {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
// 如果没有找到,返回最高质量的 LOD
|
||||
return Math.min(SIMPLE_LOD_LEVELS.length - 1, maxLodIndex)
|
||||
return SIMPLE_LOD_LEVELS.length - 1
|
||||
}
|
||||
|
||||
// 缓动函数
|
||||
private easeOutQuart(t: number): number {
|
||||
return 1 - Math.pow(1 - t, 4)
|
||||
}
|
||||
// 回滚:保持原始的 easeOutQuart 作为平滑动画曲线
|
||||
|
||||
private startAnimation(
|
||||
targetScale: number,
|
||||
@@ -829,11 +787,11 @@ export class WebGLImageViewerEngine extends ImageViewerEngineBase {
|
||||
const maxAge = 30000 // 30 秒后清理不再使用的瓦片
|
||||
|
||||
// 如果缓存过大,强制清理
|
||||
if (this.tileCache.size > this.TILE_CACHE_SIZE) {
|
||||
if (this.tileCache.size > TILE_CACHE_SIZE) {
|
||||
const tilesToRemove = Array.from(this.tileCache.entries())
|
||||
.filter(([key]) => !this.currentVisibleTiles.has(key))
|
||||
.sort(([, a], [, b]) => a.lastUsed - b.lastUsed)
|
||||
.slice(0, this.tileCache.size - this.TILE_CACHE_SIZE + 5) // 清理多一些
|
||||
.slice(0, this.tileCache.size - TILE_CACHE_SIZE + 5) // 清理多一些
|
||||
|
||||
for (const [key, tileInfo] of tilesToRemove) {
|
||||
if (tileInfo.texture) {
|
||||
@@ -870,7 +828,7 @@ export class WebGLImageViewerEngine extends ImageViewerEngineBase {
|
||||
this.pendingTileRequests.sort((a, b) => a.priority - b.priority)
|
||||
|
||||
// 限制并发加载数量
|
||||
const batch = this.pendingTileRequests.splice(0, this.MAX_TILES_PER_FRAME)
|
||||
const batch = this.pendingTileRequests.splice(0, MAX_TILES_PER_FRAME)
|
||||
|
||||
for (const request of batch) {
|
||||
const { key, priority } = request
|
||||
@@ -1095,8 +1053,8 @@ export class WebGLImageViewerEngine extends ImageViewerEngineBase {
|
||||
visibleTiles: this.currentVisibleTiles.size,
|
||||
loadingTiles: this.loadingTiles.size,
|
||||
pendingRequests: this.pendingTileRequests.length,
|
||||
cacheLimit: this.TILE_CACHE_SIZE,
|
||||
maxTilesPerFrame: this.MAX_TILES_PER_FRAME,
|
||||
cacheLimit: TILE_CACHE_SIZE,
|
||||
maxTilesPerFrame: MAX_TILES_PER_FRAME,
|
||||
tileSize: TILE_SIZE,
|
||||
cacheKeys: Array.from(this.tileCache.keys()),
|
||||
visibleKeys: Array.from(this.currentVisibleTiles),
|
||||
|
||||
@@ -1,25 +1,7 @@
|
||||
// @ts-nocheck
|
||||
/// <reference lib="webworker" />
|
||||
|
||||
// Keep only the original image Blob and parsed dimensions to avoid
|
||||
// holding a decoded 100MP ImageBitmap in memory (iOS Safari crash risk)
|
||||
let originalBlob = null
|
||||
let originalWidth = 0
|
||||
let originalHeight = 0
|
||||
let supportsRegionDecode = false
|
||||
let baseBitmap = null // fallback decoded, downscaled whole image
|
||||
|
||||
function isIOS() {
|
||||
try {
|
||||
// In workers, navigator.userAgent is available
|
||||
const ua = (self.navigator && self.navigator.userAgent) || ''
|
||||
return /iP(?:ad|hone|od)/.test(ua)
|
||||
} catch {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
const MAX_BASE_DECODE_DIM = isIOS() ? 4096 : 8192
|
||||
let originalImage = null
|
||||
|
||||
const TILE_SIZE = 512 // Must be same as in WebGLImageViewerEngine.ts
|
||||
|
||||
@@ -38,99 +20,46 @@ const WORKER_SIMPLE_LOD_LEVELS = [
|
||||
*/
|
||||
self.onmessage = async (e) => {
|
||||
const { type, payload } = e.data
|
||||
console.info('[Worker] Received message:', type)
|
||||
console.info('[Worker] Received message:', type, payload)
|
||||
|
||||
switch (type) {
|
||||
case 'load-image': {
|
||||
const { url } = payload
|
||||
try {
|
||||
console.info('[Worker] Fetching image:', url)
|
||||
const response = await fetch(url, { mode: 'cors' })
|
||||
const blob = await response.blob()
|
||||
originalImage = await createImageBitmap(blob)
|
||||
|
||||
// Parse image dimensions from headers to avoid decoding full image
|
||||
const { width, height } = await getImageSizeFromBlob(blob)
|
||||
if (!width || !height) {
|
||||
// Fallback: if we cannot parse, last resort decode just to get size
|
||||
// Note: this may be heavy; try to downscale aggressively
|
||||
const probe = await createImageBitmap(blob, {
|
||||
resizeWidth: 64,
|
||||
resizeHeight: 64,
|
||||
})
|
||||
// Width/height unknown from probe; we must bail with an error
|
||||
// and let main thread handle sizing.
|
||||
probe.close()
|
||||
throw new Error('Unsupported image format for header parsing')
|
||||
}
|
||||
|
||||
originalBlob = blob
|
||||
originalWidth = width
|
||||
originalHeight = height
|
||||
|
||||
// Feature detect region decode
|
||||
supportsRegionDecode = true
|
||||
try {
|
||||
const test = await createImageBitmap(originalBlob, 0, 0, 1, 1)
|
||||
test.close()
|
||||
} catch {
|
||||
supportsRegionDecode = false
|
||||
}
|
||||
|
||||
// If region decode is not supported, prepare a downscaled base bitmap
|
||||
if (!supportsRegionDecode) {
|
||||
const scale = Math.min(
|
||||
1,
|
||||
MAX_BASE_DECODE_DIM / Math.max(width, height),
|
||||
)
|
||||
const decW = Math.max(1, Math.round(width * scale))
|
||||
const decH = Math.max(1, Math.round(height * scale))
|
||||
baseBitmap = await createImageBitmap(originalBlob, {
|
||||
resizeWidth: decW,
|
||||
resizeHeight: decH,
|
||||
resizeQuality: 'medium',
|
||||
})
|
||||
}
|
||||
|
||||
console.info('[Worker] Image decoded, posting init-done')
|
||||
self.postMessage({ type: 'init-done' })
|
||||
|
||||
// Create a small initial LOD bitmap directly from Blob or baseBitmap
|
||||
const lodLevel = 1 // initial medium LOD
|
||||
// Create initial LOD texture
|
||||
const lodLevel = 1 // Initial LOD level
|
||||
const lodConfig = WORKER_SIMPLE_LOD_LEVELS[lodLevel]
|
||||
const finalWidth = Math.max(1, Math.round(width * lodConfig.scale))
|
||||
const finalHeight = Math.max(1, Math.round(height * lodConfig.scale))
|
||||
const finalWidth = Math.max(
|
||||
1,
|
||||
Math.round(originalImage.width * lodConfig.scale),
|
||||
)
|
||||
const finalHeight = Math.max(
|
||||
1,
|
||||
Math.round(originalImage.height * lodConfig.scale),
|
||||
)
|
||||
|
||||
let initialLODBitmap
|
||||
if (supportsRegionDecode) {
|
||||
initialLODBitmap = await createImageBitmap(originalBlob, {
|
||||
resizeWidth: finalWidth,
|
||||
resizeHeight: finalHeight,
|
||||
resizeQuality: 'medium',
|
||||
})
|
||||
} else if (baseBitmap) {
|
||||
// Scale baseBitmap again to the target LOD size
|
||||
// Use OffscreenCanvas if available, otherwise createImageBitmap(baseBitmap)
|
||||
if (typeof OffscreenCanvas !== 'undefined') {
|
||||
const canvas = new OffscreenCanvas(finalWidth, finalHeight)
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = 'medium'
|
||||
ctx.drawImage(baseBitmap, 0, 0, finalWidth, finalHeight)
|
||||
initialLODBitmap = canvas.transferToImageBitmap()
|
||||
} else {
|
||||
initialLODBitmap = await createImageBitmap(baseBitmap, {
|
||||
resizeWidth: finalWidth,
|
||||
resizeHeight: finalHeight,
|
||||
resizeQuality: 'medium',
|
||||
})
|
||||
}
|
||||
}
|
||||
const initialLODBitmap = await createImageBitmap(originalImage, {
|
||||
resizeWidth: finalWidth,
|
||||
resizeHeight: finalHeight,
|
||||
resizeQuality: 'medium',
|
||||
})
|
||||
|
||||
console.info('[Worker] Initial LOD created, posting image-loaded')
|
||||
self.postMessage(
|
||||
{
|
||||
type: 'image-loaded',
|
||||
payload: {
|
||||
imageBitmap: initialLODBitmap,
|
||||
imageWidth: width,
|
||||
imageHeight: height,
|
||||
imageWidth: originalImage.width,
|
||||
imageHeight: originalImage.height,
|
||||
lodLevel,
|
||||
},
|
||||
},
|
||||
@@ -138,21 +67,18 @@ self.onmessage = async (e) => {
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('[Worker] Error loading image:', error)
|
||||
self.postMessage({
|
||||
type: 'load-error',
|
||||
payload: { error: String(error) },
|
||||
})
|
||||
self.postMessage({ type: 'load-error', payload: { error } })
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'init': {
|
||||
// Deprecated path; keep for compatibility
|
||||
originalImage = payload.imageBitmap
|
||||
self.postMessage({ type: 'init-done' })
|
||||
break
|
||||
}
|
||||
case 'create-tile': {
|
||||
if (!originalBlob) {
|
||||
console.warn('Worker has not been initialized with a Blob.')
|
||||
if (!originalImage) {
|
||||
console.warn('Worker has not been initialized with an image.')
|
||||
return
|
||||
}
|
||||
|
||||
@@ -167,16 +93,15 @@ self.onmessage = async (e) => {
|
||||
lodConfig,
|
||||
)
|
||||
|
||||
// Compute source rect at original resolution for this tile
|
||||
const srcW = imageWidth / cols
|
||||
const srcH = imageHeight / rows
|
||||
const sourceX = Math.floor(x * srcW)
|
||||
const sourceY = Math.floor(y * srcH)
|
||||
// Calculate tile region in the original image
|
||||
const sourceWidth = imageWidth / cols
|
||||
const sourceHeight = imageHeight / rows // Assuming square tiles from a square grid on the image
|
||||
const sourceX = x * sourceWidth
|
||||
const sourceY = y * sourceHeight
|
||||
|
||||
const actualSourceWidth = Math.min(srcW, imageWidth - sourceX)
|
||||
const actualSourceHeight = Math.min(srcH, imageHeight - sourceY)
|
||||
const actualSourceWidth = Math.min(sourceWidth, imageWidth - sourceX)
|
||||
const actualSourceHeight = Math.min(sourceHeight, imageHeight - sourceY)
|
||||
|
||||
// Target tile size remains constant (<= TILE_SIZE)
|
||||
const targetWidth = Math.min(
|
||||
TILE_SIZE,
|
||||
Math.ceil(actualSourceWidth * lodConfig.scale),
|
||||
@@ -190,77 +115,33 @@ self.onmessage = async (e) => {
|
||||
return
|
||||
}
|
||||
|
||||
// Decode only the needed region, resized to the tile target size
|
||||
let imageBitmap
|
||||
if (supportsRegionDecode) {
|
||||
imageBitmap = await createImageBitmap(
|
||||
originalBlob,
|
||||
sourceX,
|
||||
sourceY,
|
||||
actualSourceWidth,
|
||||
actualSourceHeight,
|
||||
{
|
||||
resizeWidth: targetWidth,
|
||||
resizeHeight: targetHeight,
|
||||
resizeQuality: lodConfig.scale >= 1 ? 'high' : 'medium',
|
||||
},
|
||||
)
|
||||
} else if (baseBitmap) {
|
||||
// Crop from baseBitmap using canvas (preferred) or ImageBitmap crop if supported
|
||||
if (typeof OffscreenCanvas !== 'undefined') {
|
||||
const canvas = new OffscreenCanvas(targetWidth, targetHeight)
|
||||
const ctx = canvas.getContext('2d')
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = lodConfig.scale >= 1 ? 'high' : 'medium'
|
||||
// Use OffscreenCanvas to draw the tile
|
||||
const canvas = new OffscreenCanvas(targetWidth, targetHeight)
|
||||
const ctx = canvas.getContext('2d')
|
||||
|
||||
const scaleX = baseBitmap.width / originalWidth
|
||||
const scaleY = baseBitmap.height / originalHeight
|
||||
ctx.imageSmoothingEnabled = true
|
||||
ctx.imageSmoothingQuality = lodConfig.scale >= 1 ? 'high' : 'medium'
|
||||
|
||||
ctx.drawImage(
|
||||
baseBitmap,
|
||||
Math.floor(sourceX * scaleX),
|
||||
Math.floor(sourceY * scaleY),
|
||||
Math.ceil(actualSourceWidth * scaleX),
|
||||
Math.ceil(actualSourceHeight * scaleY),
|
||||
0,
|
||||
0,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
)
|
||||
imageBitmap = canvas.transferToImageBitmap()
|
||||
} else {
|
||||
// Last resort: try cropping via createImageBitmap on baseBitmap
|
||||
// Safari support may vary; wrap in try/catch
|
||||
try {
|
||||
const scaleX = baseBitmap.width / originalWidth
|
||||
const scaleY = baseBitmap.height / originalHeight
|
||||
imageBitmap = await createImageBitmap(
|
||||
baseBitmap,
|
||||
Math.floor(sourceX * scaleX),
|
||||
Math.floor(sourceY * scaleY),
|
||||
Math.ceil(actualSourceWidth * scaleX),
|
||||
Math.ceil(actualSourceHeight * scaleY),
|
||||
{
|
||||
resizeWidth: targetWidth,
|
||||
resizeHeight: targetHeight,
|
||||
resizeQuality: lodConfig.scale >= 1 ? 'high' : 'medium',
|
||||
},
|
||||
)
|
||||
} catch (e) {
|
||||
throw new Error(`Tile crop fallback failed: ${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.drawImage(
|
||||
originalImage,
|
||||
sourceX,
|
||||
sourceY,
|
||||
actualSourceWidth,
|
||||
actualSourceHeight,
|
||||
0,
|
||||
0,
|
||||
targetWidth,
|
||||
targetHeight,
|
||||
)
|
||||
|
||||
const imageBitmap = canvas.transferToImageBitmap()
|
||||
self.postMessage(
|
||||
{ type: 'tile-created', payload: { key, imageBitmap, lodLevel } },
|
||||
[imageBitmap],
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Error creating tile in worker:', error)
|
||||
self.postMessage({
|
||||
type: 'tile-error',
|
||||
payload: { key, error: String(error) },
|
||||
})
|
||||
self.postMessage({ type: 'tile-error', payload: { key, error } })
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -284,90 +165,3 @@ function getTileGridSize(imageWidth, imageHeight, _lodLevel, lodConfig) {
|
||||
|
||||
return { cols, rows }
|
||||
}
|
||||
|
||||
// Read image size from Blob without fully decoding (supports PNG, JPEG, WebP VP8X)
|
||||
async function getImageSizeFromBlob(blob) {
|
||||
// Helper to read a range from the Blob
|
||||
const read = async (start, length) => {
|
||||
const buf = await blob.slice(start, start + length).arrayBuffer()
|
||||
return new DataView(buf)
|
||||
}
|
||||
|
||||
// Read first 32 bytes to detect type
|
||||
const head = await read(0, 64)
|
||||
|
||||
// PNG: signature 89 50 4E 47 0D 0A 1A 0A, IHDR at offset 8
|
||||
if (
|
||||
head.getUint32(0, false) === 0x89504e47 &&
|
||||
head.getUint32(4, false) === 0x0d0a1a0a
|
||||
) {
|
||||
// IHDR chunk starts at 8, next 4 bytes length, then 'IHDR'
|
||||
// IHDR data: width(4), height(4) big-endian
|
||||
const ihdr = await read(16, 8)
|
||||
const width = ihdr.getUint32(0, false)
|
||||
const height = ihdr.getUint32(4, false)
|
||||
return { width, height }
|
||||
}
|
||||
|
||||
// JPEG: SOI 0xFFD8, then scan markers to SOF0/2 etc.
|
||||
if (head.getUint16(0, false) === 0xffd8) {
|
||||
let offset = 2
|
||||
const maxScan = Math.min(blob.size, 1 << 20) // scan up to 1MB
|
||||
while (offset < maxScan) {
|
||||
const dv = await read(offset, 4)
|
||||
if (dv.getUint8(0) !== 0xff) break
|
||||
const marker = dv.getUint8(1)
|
||||
const size = dv.getUint16(2, false)
|
||||
if (
|
||||
marker === 0xc0 || // SOF0
|
||||
marker === 0xc1 || // SOF1
|
||||
marker === 0xc2 || // SOF2
|
||||
marker === 0xc3 ||
|
||||
marker === 0xc5 ||
|
||||
marker === 0xc6 ||
|
||||
marker === 0xc7 ||
|
||||
marker === 0xc9 ||
|
||||
marker === 0xca ||
|
||||
marker === 0xcb ||
|
||||
marker === 0xcd ||
|
||||
marker === 0xce ||
|
||||
marker === 0xcf
|
||||
) {
|
||||
const sof = await read(offset + 5, 4)
|
||||
const height = sof.getUint16(0, false)
|
||||
const width = sof.getUint16(2, false)
|
||||
return { width, height }
|
||||
}
|
||||
offset += 2 + size
|
||||
}
|
||||
}
|
||||
|
||||
// WebP: RIFF, WEBP, VP8X chunk with canvas size
|
||||
if (head.getUint32(0, true) === 0x46464952 /* RIFF */) {
|
||||
const webpTag = await read(8, 4)
|
||||
if (webpTag.getUint32(0, false) === 0x57454250 /* 'WEBP' */) {
|
||||
// Read chunk header at 12
|
||||
const chunkHead = await read(12, 8)
|
||||
const chunkFourCC = chunkHead.getUint32(0, false)
|
||||
|
||||
// 'VP8X'
|
||||
if (chunkFourCC === 0x56503858) {
|
||||
const vp8x = await read(20, 10)
|
||||
// width-1 in 24 bits at bytes 4..6, height-1 at bytes 7..9, little-endian
|
||||
const w =
|
||||
1 +
|
||||
(vp8x.getUint8(4) |
|
||||
(vp8x.getUint8(5) << 8) |
|
||||
(vp8x.getUint8(6) << 16))
|
||||
const h =
|
||||
1 +
|
||||
(vp8x.getUint8(7) |
|
||||
(vp8x.getUint8(8) << 8) |
|
||||
(vp8x.getUint8(9) << 16))
|
||||
return { width: w, height: h }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { width: 0, height: 0 }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user