Hono图像处理:缩略图生成与水印添加

Hono图像处理:缩略图生成与水印添加

【免费下载链接】hono Fast, Lightweight, Web-standards 【免费下载链接】hono 项目地址: https://gitcode.com/GitHub_Trending/ho/hono

概述

在现代Web应用中,图像处理是一个常见需求。无论是电商平台的商品图片展示、社交媒体的用户头像处理,还是内容管理系统的图片管理,都需要对上传的图片进行自动化处理。Hono作为一个轻量级、高性能的Web框架,虽然本身不包含图像处理功能,但可以轻松集成第三方库来实现专业的图像处理能力。

本文将详细介绍如何在Hono应用中实现图像处理功能,包括缩略图生成、水印添加等常见场景,并提供完整的代码示例和最佳实践。

技术选型

图像处理库选择

对于Node.js环境,我们推荐使用以下图像处理库:

库名称特点适用场景
Sharp高性能,基于libvips生产环境,高性能要求
Jimp纯JavaScript实现简单处理,无原生依赖
Canvas基于Cairo,功能丰富复杂图形操作,水印添加

推荐方案:Sharp + Hono

Sharp是目前Node.js生态中最快、最稳定的图像处理库,特别适合在Hono这样的高性能框架中使用。

# 安装依赖
npm install sharp
npm install @types/sharp --save-dev

核心实现

1. 基础图像处理中间件

首先创建一个基础的图像处理中间件,用于处理上传的图片文件:

// src/middleware/image-processor.ts
import { Context, Next } from 'hono'
import sharp from 'sharp'

export interface ImageProcessingOptions {
  maxFileSize?: number
  allowedMimeTypes?: string[]
  defaultQuality?: number
}

export const imageProcessor = (options: ImageProcessingOptions = {}) => {
  const {
    maxFileSize = 10 * 1024 * 1024, // 10MB
    allowedMimeTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/gif'],
    defaultQuality = 80
  } = options

  return async (c: Context, next: Next) => {
    try {
      const body = await c.req.parseBody()
      const file = body.file as File

      if (!file) {
        return c.json({ error: 'No file provided' }, 400)
      }

      // 验证文件类型
      if (!allowedMimeTypes.includes(file.type)) {
        return c.json({ error: 'Unsupported file type' }, 400)
      }

      // 验证文件大小
      if (file.size > maxFileSize) {
        return c.json({ error: 'File too large' }, 400)
      }

      // 将File转换为Buffer
      const arrayBuffer = await file.arrayBuffer()
      const buffer = Buffer.from(arrayBuffer)

      // 存储图像数据到上下文
      c.set('imageBuffer', buffer)
      c.set('imageMetadata', await sharp(buffer).metadata())

      await next()
    } catch (error) {
      return c.json({ error: 'Image processing failed' }, 500)
    }
  }
}

2. 缩略图生成功能

// src/utils/thumbnail.ts
import sharp from 'sharp'

export interface ThumbnailOptions {
  width: number
  height?: number
  quality?: number
  format?: 'jpeg' | 'png' | 'webp'
  fit?: 'cover' | 'contain' | 'fill' | 'inside' | 'outside'
}

export const generateThumbnail = async (
  buffer: Buffer,
  options: ThumbnailOptions
): Promise<Buffer> => {
  const {
    width,
    height = width,
    quality = 80,
    format = 'jpeg',
    fit = 'cover'
  } = options

  return sharp(buffer)
    .resize(width, height, {
      fit,
      withoutEnlargement: true
    })
    [format]({ quality })
    .toBuffer()
}

// 多尺寸缩略图生成
export const generateMultipleThumbnails = async (
  buffer: Buffer,
  sizes: Array<{ width: number; height?: number; name: string }>
): Promise<Record<string, Buffer>> => {
  const results: Record<string, Buffer> = {}

  for (const size of sizes) {
    results[size.name] = await generateThumbnail(buffer, {
      width: size.width,
      height: size.height,
      format: 'webp',
      quality: 75
    })
  }

  return results
}

3. 水印添加功能

// src/utils/watermark.ts
import sharp from 'sharp'

export interface WatermarkOptions {
  text?: string
  imagePath?: string
  position: 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right' | 'center'
  opacity: number
  margin: number
}

export const addWatermark = async (
  buffer: Buffer,
  options: WatermarkOptions
): Promise<Buffer> => {
  const { position, opacity, margin } = options
  const image = sharp(buffer)
  const metadata = await image.metadata()

  // 计算水印位置
  const positionCoords = calculatePosition(metadata, position, margin)

  if (options.text) {
    // 文字水印
    return image
      .composite([
        {
          input: {
            text: {
              text: options.text,
              font: 'Arial',
              width: metadata.width! * 0.3,
              height: metadata.height! * 0.1,
              rgba: true,
              align: 'center'
            }
          },
          blend: 'over',
          top: positionCoords.y,
          left: positionCoords.x,
          gravity: position.replace('-', '') as any
        }
      ])
      .toBuffer()
  } else if (options.imagePath) {
    // 图片水印
    const watermarkBuffer = await sharp(options.imagePath)
      .resize(metadata.width! * 0.2) // 水印大小为原图的20%
      .toBuffer()

    return image
      .composite([
        {
          input: watermarkBuffer,
          blend: 'over',
          top: positionCoords.y,
          left: positionCoords.x,
          gravity: position.replace('-', '') as any
        }
      ])
      .toBuffer()
  }

  return buffer
}

const calculatePosition = (
  metadata: sharp.Metadata,
  position: string,
  margin: number
): { x: number; y: number } => {
  const width = metadata.width!
  const height = metadata.height!

  switch (position) {
    case 'top-left':
      return { x: margin, y: margin }
    case 'top-right':
      return { x: width - margin, y: margin }
    case 'bottom-left':
      return { x: margin, y: height - margin }
    case 'bottom-right':
      return { x: width - margin, y: height - margin }
    case 'center':
      return { x: width / 2, y: height / 2 }
    default:
      return { x: margin, y: margin }
  }
}

完整示例应用

路由配置

// src/routes/images.ts
import { Hono } from 'hono'
import { imageProcessor } from '../middleware/image-processor'
import { generateThumbnail, generateMultipleThumbnails } from '../utils/thumbnail'
import { addWatermark } from '../utils/watermark'

const app = new Hono()

// 单张缩略图生成
app.post('/thumbnail', imageProcessor(), async (c) => {
  const buffer = c.get('imageBuffer') as Buffer
  const { width, height, quality } = c.req.query()

  const thumbnail = await generateThumbnail(buffer, {
    width: parseInt(width) || 300,
    height: height ? parseInt(height) : undefined,
    quality: quality ? parseInt(quality) : 80
  })

  return c.body(thumbnail, 200, {
    'Content-Type': 'image/jpeg',
    'Content-Disposition': 'inline; filename="thumbnail.jpg"'
  })
})

// 多尺寸缩略图生成
app.post('/thumbnails', imageProcessor(), async (c) => {
  const buffer = c.get('imageBuffer') as Buffer
  
  const sizes = [
    { width: 150, height: 150, name: 'small' },
    { width: 300, height: 300, name: 'medium' },
    { width: 600, name: 'large' }
  ]

  const thumbnails = await generateMultipleThumbnails(buffer, sizes)

  // 返回JSON包含所有缩略图的Base64编码
  const result: Record<string, string> = {}
  for (const [name, thumbnailBuffer] of Object.entries(thumbnails)) {
    result[name] = thumbnailBuffer.toString('base64')
  }

  return c.json(result)
})

// 添加水印
app.post('/watermark', imageProcessor(), async (c) => {
  const buffer = c.get('imageBuffer') as Buffer
  const { text, position = 'bottom-right', opacity = '0.7', margin = '20' } = c.req.query()

  const watermarked = await addWatermark(buffer, {
    text: text || 'Sample Watermark',
    position: position as any,
    opacity: parseFloat(opacity),
    margin: parseInt(margin)
  })

  return c.body(watermarked, 200, {
    'Content-Type': 'image/jpeg',
    'Content-Disposition': 'inline; filename="watermarked.jpg"'
  })
})

// 批量处理
app.post('/process', imageProcessor(), async (c) => {
  const buffer = c.get('imageBuffer') as Buffer
  const { operations } = await c.req.json()

  let processedBuffer = buffer

  for (const operation of operations) {
    switch (operation.type) {
      case 'resize':
        processedBuffer = await sharp(processedBuffer)
          .resize(operation.width, operation.height)
          .toBuffer()
        break
      case 'crop':
        processedBuffer = await sharp(processedBuffer)
          .extract(operation)
          .toBuffer()
        break
      case 'rotate':
        processedBuffer = await sharp(processedBuffer)
          .rotate(operation.angle)
          .toBuffer()
        break
      case 'format':
        processedBuffer = await sharp(processedBuffer)
          [operation.format]({ quality: operation.quality })
          .toBuffer()
        break
    }
  }

  return c.body(processedBuffer, 200, {
    'Content-Type': 'image/jpeg'
  })
})

export default app

主应用入口

// src/index.ts
import { Hono } from 'hono'
import images from './routes/images'

const app = new Hono()

// 注册图像处理路由
app.route('/api/images', images)

// 健康检查端点
app.get('/health', (c) => c.json({ status: 'ok', timestamp: new Date().toISOString() }))

export default app

部署配置

环境变量配置

创建 .env 文件:

# 图像处理配置
MAX_FILE_SIZE=10485760
ALLOWED_MIME_TYPES=image/jpeg,image/png,image/webp
DEFAULT_QUALITY=80

# 水印配置
WATERMARK_TEXT=Powered by Hono
WATERMARK_OPACITY=0.6
WATERMARK_MARGIN=20

# 缩略图配置
THUMBNAIL_SIZES=150,300,600

Docker部署

FROM node:18-alpine

WORKDIR /app

# 安装Sharp的依赖
RUN apk add --no-cache \
    vips-dev \
    vips-tools \
    vips

COPY package*.json ./
RUN npm ci --only=production

COPY . .

EXPOSE 3000

CMD ["npm", "start"]

性能优化

1. 内存管理

// src/utils/memory-manager.ts
import { LRUCache } from 'lru-cache'

// 使用LRU缓存管理处理过的图像
const imageCache = new LRUCache<string, Buffer>({
  max: 100, // 最大缓存数量
  maxSize: 50 * 1024 * 1024, // 50MB最大内存使用
  sizeCalculation: (buffer) => buffer.length
})

export const getCachedImage = (key: string): Buffer | undefined => {
  return imageCache.get(key)
}

export const setCachedImage = (key: string, buffer: Buffer): void => {
  imageCache.set(key, buffer)
}

export const generateCacheKey = (
  buffer: Buffer,
  operations: any[]
): string => {
  const hash = require('crypto').createHash('md5')
  hash.update(buffer)
  hash.update(JSON.stringify(operations))
  return hash.digest('hex')
}

2. 流式处理

对于大文件,使用流式处理避免内存溢出:

// src/utils/stream-processor.ts
import { Readable } from 'stream'
import sharp from 'sharp'

export const processImageStream = (
  inputStream: Readable,
  operations: any[]
): Readable => {
  let processor = sharp()

  operations.forEach(operation => {
    switch (operation.type) {
      case 'resize':
        processor = processor.resize(operation.width, operation.height)
        break
      case 'format':
        processor = processor[operation.format]({ quality: operation.quality })
        break
    }
  })

  return inputStream.pipe(processor)
}

错误处理与监控

1. 错误处理中间件

// src/middleware/error-handler.ts
import { Context, Next } from 'hono'

export const errorHandler = () => async (c: Context, next: Next) => {
  try {
    await next()
  } catch (error) {
    console.error('Image processing error:', error)

【免费下载链接】hono Fast, Lightweight, Web-standards 【免费下载链接】hono 项目地址: https://gitcode.com/GitHub_Trending/ho/hono

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值