diff --git a/CHANGELOG.md b/CHANGELOG.md index 42e54e4..66746cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # 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。 diff --git a/TEST.md b/TEST.md index 68e7dec..03c13c3 100644 --- a/TEST.md +++ b/TEST.md @@ -37,9 +37,13 @@ go test -bench . | 测试项 | 耗时 (ns/op) | | :--- | :--- | -| **WarpPerspective** | 7,079,540 | -| **PHash** | 958,618 | -| **ExtractPalette** | 402,176 | +| **WarpPerspective** | 8,928,754 | +| **PHash** | 1,041,925 | +| **ExtractPalette** | 453,400 | + +## 🛡️ 鲁棒性防御 (Robustness) +- **可选参数提示**:`New`、`Save` 等方法接受指针作为可选参数,结合最新的 `go/js` 引擎智能过滤空参数。 +- **JS 错误调用栈**:JS 桥接层改用具名导出并使用 `jsmod.MakeError` 包裹错误,确保 JS 抛出异常时携带准确的 Go 运行时堆栈。 --- 所有测试均遵循 `@go` 基础设施标准,无外部系统依赖(除 FFmpeg 视频测试外,该部分会自动跳过或提示引导)。 diff --git a/go.mod b/go.mod index 0394878..4b66179 100644 --- a/go.mod +++ b/go.mod @@ -3,10 +3,10 @@ module apigo.cc/go/vision go 1.25.0 require ( - apigo.cc/go/cast v1.5.2 - apigo.cc/go/file v1.5.4 - apigo.cc/go/jsmod v1.5.2 - apigo.cc/go/rand v1.5.2 + apigo.cc/go/cast v1.5.3 + apigo.cc/go/file v1.5.5 + apigo.cc/go/jsmod v1.5.3 + apigo.cc/go/rand v1.5.3 github.com/boombuler/barcode v1.1.0 github.com/disintegration/imaging v1.6.2 github.com/flopp/go-findfont v0.1.0 @@ -17,8 +17,8 @@ require ( ) require ( - apigo.cc/go/encoding v1.5.3 - apigo.cc/go/safe v1.5.1 + apigo.cc/go/encoding v1.5.4 + apigo.cc/go/safe v1.5.2 github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect github.com/kr/text v0.2.0 // indirect golang.org/x/crypto v0.52.0 // indirect diff --git a/js_export.go b/js_export.go index 174f556..44ab17b 100644 --- a/js_export.go +++ b/js_export.go @@ -10,115 +10,169 @@ import ( func init() { jsmod.Register("vision", map[string]any{ // 基础操作 - "Load": func(ctx context.Context, path string) (*jsCanvas, error) { - p, err := file.VerifyPathForSafeMode(ctx, path) - if err != nil { - return nil, err - } - c, err := Load(p) - if err != nil { - return nil, err - } - return &jsCanvas{ctx: ctx, c: c}, nil - }, - "New": func(ctx context.Context, w, h int, bg *string) *jsCanvas { - if bg != nil { - return &jsCanvas{ctx: ctx, c: New(w, h, *bg)} - } - return &jsCanvas{ctx: ctx, c: New(w, h)} - }, - "Save": func(ctx context.Context, j *jsCanvas, path string, quality *int) error { - p, err := file.VerifyPathForSafeMode(ctx, path) - if err != nil { - return err - } - if quality != nil { - return Save(j.c, p, *quality) - } - return Save(j.c, p) - }, + "Load": jsLoad, + "New": jsNew, + "Save": jsSave, // 生成类 - "GenerateQRCode": func(ctx context.Context, content string, size int) (*jsCanvas, error) { - c, err := GenerateQRCode(content, size) - if err != nil { - return nil, err - } - return &jsCanvas{ctx: ctx, c: c}, nil - }, - "GenerateBarcode": func(ctx context.Context, content string, w, h int) (*jsCanvas, error) { - c, err := GenerateBarcode(content, w, h) - if err != nil { - return nil, err - } - return &jsCanvas{ctx: ctx, c: c}, nil - }, - "GenerateCaptcha": func(ctx context.Context, opt *CaptchaOption) *jsCanvas { - return &jsCanvas{ctx: ctx, c: GenerateCaptcha(opt)} - }, + "GenerateQRCode": jsGenerateQRCode, + "GenerateBarcode": jsGenerateBarcode, + "GenerateCaptcha": jsGenerateCaptcha, // 预览类 (直接写文件) - "GenerateImagePreview": func(ctx context.Context, src, out string, w, h int) error { - pSrc, err := file.VerifyPathForSafeMode(ctx, src) - if err != nil { - return err - } - pOut, err := file.VerifyPathForSafeMode(ctx, out) - if err != nil { - return err - } - return GenerateImagePreview(pSrc, pOut, w, h) - }, - "GenerateVideoPreview": func(ctx context.Context, src, out string, w, h int, interval *int) error { - pSrc, err := file.VerifyPathForSafeMode(ctx, src) - if err != nil { - return err - } - pOut, err := file.VerifyPathForSafeMode(ctx, out) - if err != nil { - return err - } - if interval != nil { - return GenerateVideoPreview(pSrc, pOut, w, h, *interval) - } - return GenerateVideoPreview(pSrc, pOut, w, h) - }, + "GenerateImagePreview": jsGenerateImagePreview, + "GenerateVideoPreview": jsGenerateVideoPreview, // 转换类 - "Convert": func(ctx context.Context, src, dst string, quality *int) error { - pSrc, err := file.VerifyPathForSafeMode(ctx, src) - if err != nil { - return err - } - pDst, err := file.VerifyPathForSafeMode(ctx, dst) - if err != nil { - return err - } - if quality != nil { - return Convert(pSrc, pDst, *quality) - } - return Convert(pSrc, pDst) - }, - "Optimize": func(ctx context.Context, path string, maxWidth int, quality int) error { - p, err := file.VerifyPathForSafeMode(ctx, path) - if err != nil { - return err - } - return Optimize(p, maxWidth, quality) - }, + "Convert": jsConvert, + "Optimize": jsOptimize, // 辅助工具 - "LoadFonts": LoadFonts, - "Recognize": func(ctx context.Context, path string) (string, error) { - p, err := file.VerifyPathForSafeMode(ctx, path) - if err != nil { - return "", err - } - return Recognize(p) - }, + "LoadFonts": jsLoadFonts, + "Recognize": jsRecognize, }) } +func jsLoad(ctx context.Context, path string) (*jsCanvas, error) { + p, err := file.VerifyPathForSafeMode(ctx, path) + if err != nil { + return nil, jsmod.MakeError(err) + } + c, err := Load(p) + if err != nil { + return nil, jsmod.MakeError(err) + } + return &jsCanvas{ctx: ctx, c: c}, nil +} + +func jsNew(ctx context.Context, w, h int, bg *string) *jsCanvas { + if bg != nil { + return &jsCanvas{ctx: ctx, c: New(w, h, *bg)} + } + return &jsCanvas{ctx: ctx, c: New(w, h)} +} + +func jsSave(ctx context.Context, j *jsCanvas, path string, quality *int) error { + p, err := file.VerifyPathForSafeMode(ctx, path) + if err != nil { + return jsmod.MakeError(err) + } + if quality != nil { + 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 +} + +func jsGenerateQRCode(ctx context.Context, content string, size int) (*jsCanvas, error) { + c, err := GenerateQRCode(content, size) + if err != nil { + return nil, jsmod.MakeError(err) + } + return &jsCanvas{ctx: ctx, c: c}, nil +} + +func jsGenerateBarcode(ctx context.Context, content string, w, h int) (*jsCanvas, error) { + c, err := GenerateBarcode(content, w, h) + if err != nil { + return nil, jsmod.MakeError(err) + } + return &jsCanvas{ctx: ctx, c: c}, nil +} + +func jsGenerateCaptcha(ctx context.Context, opt *CaptchaOption) *jsCanvas { + 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) + if err != nil { + return jsmod.MakeError(err) + } + pOut, err := file.VerifyPathForSafeMode(ctx, out) + if err != nil { + return jsmod.MakeError(err) + } + if err := GenerateImagePreview(pSrc, pOut, w, h); err != nil { + return jsmod.MakeError(err) + } + return nil +} + +func jsGenerateVideoPreview(ctx context.Context, src, out string, w, h int, interval *int) error { + pSrc, err := file.VerifyPathForSafeMode(ctx, src) + if err != nil { + return jsmod.MakeError(err) + } + pOut, err := file.VerifyPathForSafeMode(ctx, out) + if err != nil { + return jsmod.MakeError(err) + } + if interval != nil { + 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 +} + +func jsConvert(ctx context.Context, src, dst string, quality *int) error { + pSrc, err := file.VerifyPathForSafeMode(ctx, src) + if err != nil { + return jsmod.MakeError(err) + } + pDst, err := file.VerifyPathForSafeMode(ctx, dst) + if err != nil { + return jsmod.MakeError(err) + } + if quality != nil { + if err := Convert(pSrc, pDst, *quality); err != nil { + return jsmod.MakeError(err) + } + return nil + } + if err := Convert(pSrc, pDst); err != nil { + return jsmod.MakeError(err) + } + return nil +} + +func jsOptimize(ctx context.Context, path string, maxWidth int, quality int) error { + p, err := file.VerifyPathForSafeMode(ctx, path) + if err != nil { + return jsmod.MakeError(err) + } + 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 包装器,支持链式调用 type jsCanvas struct { ctx context.Context @@ -131,27 +185,39 @@ func (j *jsCanvas) Height() int { return j.c.Height() } func (j *jsCanvas) Save(path string, quality *int) error { p, err := file.VerifyPathForSafeMode(j.ctx, path) if err != nil { - return err + return jsmod.MakeError(err) } if quality != nil { - return Save(j.c, p, *quality) + if err := Save(j.c, p, *quality); err != nil { + return jsmod.MakeError(err) + } + return nil } - return Save(j.c, p) + if err := Save(j.c, p); err != nil { + return jsmod.MakeError(err) + } + return nil } // 效果与绘图 (返回自身以支持链式) -func (j *jsCanvas) Resize(w, h int) *jsCanvas { j.c.Resize(w, h); return j } -func (j *jsCanvas) Blur(sigma float64) *jsCanvas { j.c.Blur(sigma); return j } -func (j *jsCanvas) Grayscale() *jsCanvas { j.c.Grayscale(); return j } -func (j *jsCanvas) Invert() *jsCanvas { j.c.Invert(); return j } -func (j *jsCanvas) Rotate(angle float64) *jsCanvas { j.c.Rotate(angle); return j } -func (j *jsCanvas) FlipH() *jsCanvas { j.c.FlipH(); return j } -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) Resize(w, h int) *jsCanvas { j.c.Resize(w, h); return j } +func (j *jsCanvas) Blur(sigma float64) *jsCanvas { j.c.Blur(sigma); return j } +func (j *jsCanvas) Grayscale() *jsCanvas { j.c.Grayscale(); return j } +func (j *jsCanvas) Invert() *jsCanvas { j.c.Invert(); return j } +func (j *jsCanvas) Rotate(angle float64) *jsCanvas { j.c.Rotate(angle); return j } +func (j *jsCanvas) FlipH() *jsCanvas { j.c.FlipH(); return j } +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) 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) Circle(x, y, r float64, opt *DrawStyle) *jsCanvas { j.c.Circle(x, y, r, opt); 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) 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 { j.c.Line(x1, y1, x2, y2, opt) 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) Recognize() (string, error) { return j.c.Recognize() } -func (j *jsCanvas) DecodeQRCode() (string, error) { return j.c.DecodeQRCode() } +func (j *jsCanvas) DecodeAll() (string, error) { + res, err := j.c.DecodeAll() + 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 +}