Compare commits

..

3 Commits

Author SHA1 Message Date
AI Engineer
84e72eec65 feat(vision): 具名化 JS 导出并动态包裹错误(by AI) 2026-06-21 10:18:46 +08:00
AI Engineer
9f06dbedb4 chore: align infrastructure to v1.5.x (by AI) 2026-06-11 20:50:04 +08:00
AI Engineer
28e93c297d chore: align infrastructure to v1.5.2 (by AI) 2026-06-11 19:02:36 +08:00
4 changed files with 218 additions and 123 deletions

View File

@ -1,5 +1,12 @@
# CHANGELOG - apigo.cc/go/vision # CHANGELOG - apigo.cc/go/vision
## v1.5.3 (2026-06-21)
- **JS 对齐**: 重构 JS 导出为具名函数,并引入 `jsmod.MakeError` 动态包装错误以获取调用栈。
- **依赖更新**: 升级依赖 `jsmod``v1.5.3``cast``v1.5.3``rand``v1.5.3``encoding``v1.5.4``safe``v1.5.2``file``v1.5.5`
## v1.5.2 (2026-06-11)
- **版本对齐**: 基础设施全局对齐 v1.5.2。
## v1.5.1 (2026-06-08) ## v1.5.1 (2026-06-08)
- **JS 对齐 & 智能文档**: - **JS 对齐 & 智能文档**:
- 将所有注册到 `jsmod` 的方法名统一为 PascalCase。 - 将所有注册到 `jsmod` 的方法名统一为 PascalCase。

10
TEST.md
View File

@ -37,9 +37,13 @@ go test -bench .
| 测试项 | 耗时 (ns/op) | | 测试项 | 耗时 (ns/op) |
| :--- | :--- | | :--- | :--- |
| **WarpPerspective** | 7,079,540 | | **WarpPerspective** | 8,928,754 |
| **PHash** | 958,618 | | **PHash** | 1,041,925 |
| **ExtractPalette** | 402,176 | | **ExtractPalette** | 453,400 |
## 🛡️ 鲁棒性防御 (Robustness)
- **可选参数提示**`New``Save` 等方法接受指针作为可选参数,结合最新的 `go/js` 引擎智能过滤空参数。
- **JS 错误调用栈**JS 桥接层改用具名导出并使用 `jsmod.MakeError` 包裹错误,确保 JS 抛出异常时携带准确的 Go 运行时堆栈。
--- ---
所有测试均遵循 `@go` 基础设施标准,无外部系统依赖(除 FFmpeg 视频测试外,该部分会自动跳过或提示引导)。 所有测试均遵循 `@go` 基础设施标准,无外部系统依赖(除 FFmpeg 视频测试外,该部分会自动跳过或提示引导)。

12
go.mod
View File

@ -3,10 +3,10 @@ module apigo.cc/go/vision
go 1.25.0 go 1.25.0
require ( require (
apigo.cc/go/cast v1.5.0 apigo.cc/go/cast v1.5.3
apigo.cc/go/file v1.5.0 apigo.cc/go/file v1.5.5
apigo.cc/go/jsmod v1.5.0 apigo.cc/go/jsmod v1.5.3
apigo.cc/go/rand v1.5.0 apigo.cc/go/rand v1.5.3
github.com/boombuler/barcode v1.1.0 github.com/boombuler/barcode v1.1.0
github.com/disintegration/imaging v1.6.2 github.com/disintegration/imaging v1.6.2
github.com/flopp/go-findfont v0.1.0 github.com/flopp/go-findfont v0.1.0
@ -17,8 +17,8 @@ require (
) )
require ( require (
apigo.cc/go/encoding v1.5.0 // indirect apigo.cc/go/encoding v1.5.4
apigo.cc/go/safe v1.5.0 // indirect apigo.cc/go/safe v1.5.2
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
github.com/kr/text v0.2.0 // indirect github.com/kr/text v0.2.0 // indirect
golang.org/x/crypto v0.52.0 // indirect golang.org/x/crypto v0.52.0 // indirect

View File

@ -10,113 +10,167 @@ import (
func init() { func init() {
jsmod.Register("vision", map[string]any{ jsmod.Register("vision", map[string]any{
// 基础操作 // 基础操作
"Load": func(ctx context.Context, path string) (*jsCanvas, error) { "Load": jsLoad,
"New": jsNew,
"Save": jsSave,
// 生成类
"GenerateQRCode": jsGenerateQRCode,
"GenerateBarcode": jsGenerateBarcode,
"GenerateCaptcha": jsGenerateCaptcha,
// 预览类 (直接写文件)
"GenerateImagePreview": jsGenerateImagePreview,
"GenerateVideoPreview": jsGenerateVideoPreview,
// 转换类
"Convert": jsConvert,
"Optimize": jsOptimize,
// 辅助工具
"LoadFonts": jsLoadFonts,
"Recognize": jsRecognize,
})
}
func jsLoad(ctx context.Context, path string) (*jsCanvas, error) {
p, err := file.VerifyPathForSafeMode(ctx, path) p, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil { if err != nil {
return nil, err return nil, jsmod.MakeError(err)
} }
c, err := Load(p) c, err := Load(p)
if err != nil { if err != nil {
return nil, err return nil, jsmod.MakeError(err)
} }
return &jsCanvas{ctx: ctx, c: c}, nil return &jsCanvas{ctx: ctx, c: c}, nil
}, }
"New": func(ctx context.Context, w, h int, bg *string) *jsCanvas {
func jsNew(ctx context.Context, w, h int, bg *string) *jsCanvas {
if bg != nil { if bg != nil {
return &jsCanvas{ctx: ctx, c: New(w, h, *bg)} return &jsCanvas{ctx: ctx, c: New(w, h, *bg)}
} }
return &jsCanvas{ctx: ctx, c: New(w, h)} return &jsCanvas{ctx: ctx, c: New(w, h)}
}, }
"Save": func(ctx context.Context, j *jsCanvas, path string, quality *int) error {
func jsSave(ctx context.Context, j *jsCanvas, path string, quality *int) error {
p, err := file.VerifyPathForSafeMode(ctx, path) p, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil { if err != nil {
return err return jsmod.MakeError(err)
} }
if quality != nil { if quality != nil {
return Save(j.c, p, *quality) if err := Save(j.c, p, *quality); err != nil {
return jsmod.MakeError(err)
}
return nil
}
if err := Save(j.c, p); err != nil {
return jsmod.MakeError(err)
}
return nil
} }
return Save(j.c, p)
},
// 生成类 func jsGenerateQRCode(ctx context.Context, content string, size int) (*jsCanvas, error) {
"GenerateQRCode": func(ctx context.Context, content string, size int) (*jsCanvas, error) {
c, err := GenerateQRCode(content, size) c, err := GenerateQRCode(content, size)
if err != nil { if err != nil {
return nil, err return nil, jsmod.MakeError(err)
} }
return &jsCanvas{ctx: ctx, c: c}, nil return &jsCanvas{ctx: ctx, c: c}, nil
}, }
"GenerateBarcode": func(ctx context.Context, content string, w, h int) (*jsCanvas, error) {
func jsGenerateBarcode(ctx context.Context, content string, w, h int) (*jsCanvas, error) {
c, err := GenerateBarcode(content, w, h) c, err := GenerateBarcode(content, w, h)
if err != nil { if err != nil {
return nil, err return nil, jsmod.MakeError(err)
} }
return &jsCanvas{ctx: ctx, c: c}, nil return &jsCanvas{ctx: ctx, c: c}, nil
}, }
"GenerateCaptcha": func(ctx context.Context, opt *CaptchaOption) *jsCanvas {
return &jsCanvas{ctx: ctx, c: GenerateCaptcha(opt)}
},
// 预览类 (直接写文件) func jsGenerateCaptcha(ctx context.Context, opt *CaptchaOption) *jsCanvas {
"GenerateImagePreview": func(ctx context.Context, src, out string, w, h int) error { return &jsCanvas{ctx: ctx, c: GenerateCaptcha(opt)}
}
func jsGenerateImagePreview(ctx context.Context, src, out string, w, h int) error {
pSrc, err := file.VerifyPathForSafeMode(ctx, src) pSrc, err := file.VerifyPathForSafeMode(ctx, src)
if err != nil { if err != nil {
return err return jsmod.MakeError(err)
} }
pOut, err := file.VerifyPathForSafeMode(ctx, out) pOut, err := file.VerifyPathForSafeMode(ctx, out)
if err != nil { if err != nil {
return err return jsmod.MakeError(err)
} }
return GenerateImagePreview(pSrc, pOut, w, h) if err := GenerateImagePreview(pSrc, pOut, w, h); err != nil {
}, return jsmod.MakeError(err)
"GenerateVideoPreview": func(ctx context.Context, src, out string, w, h int, interval *int) error { }
return nil
}
func jsGenerateVideoPreview(ctx context.Context, src, out string, w, h int, interval *int) error {
pSrc, err := file.VerifyPathForSafeMode(ctx, src) pSrc, err := file.VerifyPathForSafeMode(ctx, src)
if err != nil { if err != nil {
return err return jsmod.MakeError(err)
} }
pOut, err := file.VerifyPathForSafeMode(ctx, out) pOut, err := file.VerifyPathForSafeMode(ctx, out)
if err != nil { if err != nil {
return err return jsmod.MakeError(err)
} }
if interval != nil { if interval != nil {
return GenerateVideoPreview(pSrc, pOut, w, h, *interval) if err := GenerateVideoPreview(pSrc, pOut, w, h, *interval); err != nil {
return jsmod.MakeError(err)
}
return nil
}
if err := GenerateVideoPreview(pSrc, pOut, w, h); err != nil {
return jsmod.MakeError(err)
}
return nil
} }
return GenerateVideoPreview(pSrc, pOut, w, h)
},
// 转换类 func jsConvert(ctx context.Context, src, dst string, quality *int) error {
"Convert": func(ctx context.Context, src, dst string, quality *int) error {
pSrc, err := file.VerifyPathForSafeMode(ctx, src) pSrc, err := file.VerifyPathForSafeMode(ctx, src)
if err != nil { if err != nil {
return err return jsmod.MakeError(err)
} }
pDst, err := file.VerifyPathForSafeMode(ctx, dst) pDst, err := file.VerifyPathForSafeMode(ctx, dst)
if err != nil { if err != nil {
return err return jsmod.MakeError(err)
} }
if quality != nil { if quality != nil {
return Convert(pSrc, pDst, *quality) if err := Convert(pSrc, pDst, *quality); err != nil {
return jsmod.MakeError(err)
} }
return Convert(pSrc, pDst) return nil
}, }
"Optimize": func(ctx context.Context, path string, maxWidth int, quality int) error { if err := Convert(pSrc, pDst); err != nil {
p, err := file.VerifyPathForSafeMode(ctx, path) return jsmod.MakeError(err)
if err != nil { }
return err return nil
} }
return Optimize(p, maxWidth, quality)
},
// 辅助工具 func jsOptimize(ctx context.Context, path string, maxWidth int, quality int) error {
"LoadFonts": LoadFonts,
"Recognize": func(ctx context.Context, path string) (string, error) {
p, err := file.VerifyPathForSafeMode(ctx, path) p, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil { if err != nil {
return "", err return jsmod.MakeError(err)
} }
return Recognize(p) if err := Optimize(p, maxWidth, quality); err != nil {
}, return jsmod.MakeError(err)
}) }
return nil
}
func jsLoadFonts(paths ...string) {
LoadFonts(paths...)
}
func jsRecognize(ctx context.Context, path string) (string, error) {
p, err := file.VerifyPathForSafeMode(ctx, path)
if err != nil {
return "", jsmod.MakeError(err)
}
res, err := Recognize(p)
if err != nil {
return "", jsmod.MakeError(err)
}
return res, nil
} }
// jsCanvas 包装器,支持链式调用 // jsCanvas 包装器,支持链式调用
@ -131,12 +185,18 @@ func (j *jsCanvas) Height() int { return j.c.Height() }
func (j *jsCanvas) Save(path string, quality *int) error { func (j *jsCanvas) Save(path string, quality *int) error {
p, err := file.VerifyPathForSafeMode(j.ctx, path) p, err := file.VerifyPathForSafeMode(j.ctx, path)
if err != nil { if err != nil {
return err return jsmod.MakeError(err)
} }
if quality != nil { if quality != nil {
return Save(j.c, p, *quality) if err := Save(j.c, p, *quality); err != nil {
return jsmod.MakeError(err)
} }
return Save(j.c, p) return nil
}
if err := Save(j.c, p); err != nil {
return jsmod.MakeError(err)
}
return nil
} }
// 效果与绘图 (返回自身以支持链式) // 效果与绘图 (返回自身以支持链式)
@ -150,8 +210,14 @@ func (j *jsCanvas) FlipV() *jsCanvas { j.c.FlipV(); return j }
func (j *jsCanvas) Sharpen(sigma float64) *jsCanvas { j.c.Sharpen(sigma); return j } func (j *jsCanvas) Sharpen(sigma float64) *jsCanvas { j.c.Sharpen(sigma); return j }
func (j *jsCanvas) AdjustBrightness(p float64) *jsCanvas { j.c.AdjustBrightness(p); return j } func (j *jsCanvas) AdjustBrightness(p float64) *jsCanvas { j.c.AdjustBrightness(p); return j }
func (j *jsCanvas) Rect(x, y, w, h float64, opt *DrawStyle) *jsCanvas { j.c.Rect(x, y, w, h, opt); return j } func (j *jsCanvas) Rect(x, y, w, h float64, opt *DrawStyle) *jsCanvas {
func (j *jsCanvas) Circle(x, y, r float64, opt *DrawStyle) *jsCanvas { j.c.Circle(x, y, r, opt); return j } j.c.Rect(x, y, w, h, opt)
return j
}
func (j *jsCanvas) Circle(x, y, r float64, opt *DrawStyle) *jsCanvas {
j.c.Circle(x, y, r, opt)
return j
}
func (j *jsCanvas) Line(x1, y1, x2, y2 float64, opt *DrawStyle) *jsCanvas { func (j *jsCanvas) Line(x1, y1, x2, y2 float64, opt *DrawStyle) *jsCanvas {
j.c.Line(x1, y1, x2, y2, opt) j.c.Line(x1, y1, x2, y2, opt)
return j return j
@ -162,6 +228,24 @@ func (j *jsCanvas) DrawText(x, y float64, text string, opt *TextOption) *jsCanva
} }
// 识别类 // 识别类
func (j *jsCanvas) DecodeAll() (string, error) { return j.c.DecodeAll() } func (j *jsCanvas) DecodeAll() (string, error) {
func (j *jsCanvas) Recognize() (string, error) { return j.c.Recognize() } res, err := j.c.DecodeAll()
func (j *jsCanvas) DecodeQRCode() (string, error) { return j.c.DecodeQRCode() } if err != nil {
return "", jsmod.MakeError(err)
}
return res, nil
}
func (j *jsCanvas) Recognize() (string, error) {
res, err := j.c.Recognize()
if err != nil {
return "", jsmod.MakeError(err)
}
return res, nil
}
func (j *jsCanvas) DecodeQRCode() (string, error) {
res, err := j.c.DecodeQRCode()
if err != nil {
return "", jsmod.MakeError(err)
}
return res, nil
}