// Package imagecheck 提供图片尺寸校验,用于在上传/解码前拦截"解压炸弹"。 package imagecheck import ( "errors" "fmt" "image" _ "image/gif" _ "image/jpeg" _ "image/png" "io" "math" ) // maxImageSide 是单边最大像素,MaxImagePixels 为其平方。 const maxImageSide = 4096 // MaxImageSide 导出单边最大像素,供上层在错误提示中向用户展示具体限制。 const MaxImageSide = maxImageSide // MaxImagePixels 限制解码后的最大像素总量(4096×4096),用于拦截"解压炸弹": // 超大纯色 PNG/GIF 压缩后体积很小,但 image.Decode 会按宽×高分配内存导致 OOM。 const MaxImagePixels = maxImageSide * maxImageSide // ErrImageTooLarge 表示图片像素总量超过 MaxImagePixels, // 是所有"图片尺寸超限"的统一错误,供各上传入口识别并展示统一文案。 var ErrImageTooLarge = errors.New(fmt.Sprintf( "图片尺寸过大,最大支持 %d×%d 像素,请压缩后重新上传", maxImageSide, maxImageSide, )) // CheckPixelLimit 仅读取图片头部(image.DecodeConfig)校验宽高与像素总量, // 在完整解码前拦截超大图片,避免内存耗尽。 // // 说明:对 GIF 同样适用——image.DecodeConfig 对 GIF 只读取逻辑屏幕宽高(不解码帧), // 成本极低;但大逻辑屏幕的 GIF 在后续 image.Decode 时仍会按屏幕尺寸分配内存,故必须校验。 // // 注意:该方法会读取(消耗)传入的 reader,调用方需自行重建/复位 reader。 func CheckPixelLimit(r io.Reader) error { cfg, _, err := image.DecodeConfig(r) if err != nil { return err } if cfg.Width <= 0 || cfg.Height <= 0 { return nil } if int64(cfg.Width)*int64(cfg.Height) > MaxImagePixels { return ErrImageTooLarge } return nil } // ErrBadAspectRatio 表示图片宽高比不符合用途要求(如头像必须 1:1、背景必须 16:9)。 // 由 storage 在头像/背景上传入口校验后返回,route 层负责映射为对应语言的提示文案。 var ErrBadAspectRatio = errors.New("image aspect ratio does not meet the requirement") // DefaultAspectTolerance 是宽高比允许的绝对偏差。 // 客户端裁剪输出即为精确比例,此处仅用于拦截绕过裁剪直传的明显不符图片, // 因此使用窄容差(±0.01);宽高比越界(如 4:3 传成 16:9)会稳定触发拒绝。 const DefaultAspectTolerance = 0.01 // CheckAspectRatio 仅读取图片头部校验宽高比是否接近 wantW:wantH(绝对偏差 ≤ tolerance)。 // 与 CheckPixelLimit 一致:只做 DecodeConfig 级轻量检查,不解码全图。 // 注意:该方法会读取(消耗)传入的 reader,调用方需自行重建/复位 reader。 func CheckAspectRatio(r io.Reader, wantW, wantH float64, tolerance float64) error { cfg, _, err := image.DecodeConfig(r) if err != nil { return err } return CheckAspectRatioSize(cfg.Width, cfg.Height, wantW, wantH, tolerance) } // CheckAspectRatioSize 是 CheckAspectRatio 的「宽高已知」版本。 // 调用方若已从文件头拿到尺寸(或需要先按 EXIF Orientation 修正宽高后再比对), // 用本函数可避免重复读取文件头。 func CheckAspectRatioSize(w, h int, wantW, wantH float64, tolerance float64) error { if wantW <= 0 || wantH <= 0 { return nil } if w <= 0 || h <= 0 { return nil } actual := float64(w) / float64(h) if math.Abs(actual-wantW/wantH) > tolerance { return ErrBadAspectRatio } return nil }