From 4daea01c60f1d73265f8242a00b452838b9a989b Mon Sep 17 00:00:00 2001 From: Jiyong Huang Date: Tue, 23 Jul 2024 16:43:07 +0800 Subject: [PATCH] feat(ext): restore image and geo functions Signed-off-by: Jiyong Huang --- extensions/functions/geohash/geohash.go | 296 ++++++++++++ extensions/functions/geohash/geohash.json | 479 ++++++++++++++++++++ extensions/functions/image/exports.go | 20 + extensions/functions/image/image.json | 175 +++++++ extensions/functions/image/img.png | Bin 0 -> 47410 bytes extensions/functions/image/resize.go | 103 +++++ extensions/functions/image/resize_test.go | 95 ++++ extensions/functions/image/resized.png | Bin 0 -> 3158 bytes extensions/functions/image/thumbnail.go | 75 +++ extensions/functions/image/thumnail_test.go | 81 ++++ go.mod | 1 + go.sum | 2 + 12 files changed, 1327 insertions(+) create mode 100644 extensions/functions/geohash/geohash.go create mode 100644 extensions/functions/geohash/geohash.json create mode 100644 extensions/functions/image/exports.go create mode 100644 extensions/functions/image/image.json create mode 100644 extensions/functions/image/img.png create mode 100644 extensions/functions/image/resize.go create mode 100644 extensions/functions/image/resize_test.go create mode 100644 extensions/functions/image/resized.png create mode 100644 extensions/functions/image/thumbnail.go create mode 100644 extensions/functions/image/thumnail_test.go diff --git a/extensions/functions/geohash/geohash.go b/extensions/functions/geohash/geohash.go new file mode 100644 index 0000000000..128cffcac0 --- /dev/null +++ b/extensions/functions/geohash/geohash.go @@ -0,0 +1,296 @@ +// Copyright 2021-2024 EMQ Technologies Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "fmt" + + "github.com/mmcloughlin/geohash" + + "github.com/lf-edge/ekuiper/contract/v2/api" +) + +type ( + geohashEncode struct{} + geohashEncodeInt struct{} + geohashDecode struct{} + geohashDecodeInt struct{} + geohashBoundingBox struct{} + geohashBoundingBoxInt struct{} + geohashNeighbor struct{} + geohashNeighborInt struct{} + geohashNeighbors struct{} + geohashNeighborsInt struct{} + position struct { + Longitude float64 + Latitude float64 + } +) + +var ( + GeohashEncode geohashEncode + GeohashEncodeInt geohashEncodeInt + GeohashDecode geohashDecode + GeohashDecodeInt geohashDecodeInt + GeohashBoundingBox geohashBoundingBox + GeohashBoundingBoxInt geohashBoundingBoxInt + GeohashNeighbor geohashNeighbor + GeohashNeighborInt geohashNeighborInt + GeohashNeighbors geohashNeighbors + GeohashNeighborsInt geohashNeighborsInt + g_direction = map[string]geohash.Direction{ + "North": geohash.North, + "NorthEast": geohash.NorthEast, + "East": geohash.East, + "SouthEast": geohash.SouthEast, + "South": geohash.South, + "SouthWest": geohash.SouthWest, + "West": geohash.West, + "NorthWest": geohash.NorthWest, + } +) + +func (r *geohashEncode) IsAggregate() bool { + return false +} + +func (r *geohashEncodeInt) IsAggregate() bool { + return false +} + +func (r *geohashDecode) IsAggregate() bool { + return false +} + +func (r *geohashDecodeInt) IsAggregate() bool { + return false +} + +func (r *geohashBoundingBox) IsAggregate() bool { + return false +} + +func (r *geohashBoundingBoxInt) IsAggregate() bool { + return false +} + +func (r *geohashNeighbor) IsAggregate() bool { + return false +} + +func (r *geohashNeighborInt) IsAggregate() bool { + return false +} + +func (r *geohashNeighbors) IsAggregate() bool { + return false +} + +func (r *geohashNeighborsInt) IsAggregate() bool { + return false +} + +func (r *geohashEncode) Validate(args []any) error { + if len(args) != 2 { + return fmt.Errorf("The geohashEncode function supports 2 parameters, but got %d", len(args)) + } + return nil +} + +func (r *geohashEncodeInt) Validate(args []any) error { + if len(args) != 2 { + return fmt.Errorf("The geohashEncodeInt function supports 2 parameters, but got %d", len(args)) + } + return nil +} + +func (r *geohashDecode) Validate(args []any) error { + if len(args) != 1 { + return fmt.Errorf("The geohashDecode function supports 1 parameters, but got %d", len(args)) + } + return nil +} + +func (r *geohashDecodeInt) Validate(args []any) error { + if len(args) != 1 { + return fmt.Errorf("The geohashDecodeInt function supports 1 parameters, but got %d", len(args)) + } + return nil +} + +func (r *geohashBoundingBox) Validate(args []any) error { + if len(args) != 1 { + return fmt.Errorf("The geohashBoundingBox function supports 1 parameters, but got %d", len(args)) + } + return nil +} + +func (r *geohashBoundingBoxInt) Validate(args []any) error { + if len(args) != 1 { + return fmt.Errorf("The geohashBoundingBoxInt function supports 1 parameters, but got %d", len(args)) + } + return nil +} + +func (r *geohashNeighbor) Validate(args []any) error { + if len(args) != 2 { + return fmt.Errorf("The geohashNeighbor function supports 2 parameters, but got %d", len(args)) + } + return nil +} + +func (r *geohashNeighborInt) Validate(args []any) error { + if len(args) != 2 { + return fmt.Errorf("The geohashNeighborInt function supports 2 parameters, but got %d", len(args)) + } + return nil +} + +func (r *geohashNeighbors) Validate(args []any) error { + if len(args) != 1 { + return fmt.Errorf("The geohashNeighbors function supports 1 parameters, but got %d", len(args)) + } + return nil +} + +func (r *geohashNeighborsInt) Validate(args []any) error { + if len(args) != 1 { + return fmt.Errorf("The geohashNeighborsInt function supports 1 parameters, but got %d", len(args)) + } + return nil +} + +func (r *geohashEncode) Exec(args []any, _ api.FunctionContext) (any, bool) { + la, ok := args[0].(float64) + if !ok { + return fmt.Errorf("arg[0] is not a float, got %v", args[0]), false + } + lo, ok := args[1].(float64) + if !ok { + return fmt.Errorf("arg[1] is not a float, got %v", args[1]), false + } + return geohash.Encode(la, lo), true +} + +func (r *geohashEncodeInt) Exec(args []any, _ api.FunctionContext) (any, bool) { + la, ok := args[0].(float64) + if !ok { + return fmt.Errorf("arg[0] is not a float, got %v", args[0]), false + } + lo, ok := args[1].(float64) + if !ok { + return fmt.Errorf("arg[1] is not a float, got %v", args[1]), false + } + return geohash.EncodeInt(la, lo), true +} + +func (r *geohashDecode) Exec(args []any, _ api.FunctionContext) (any, bool) { + hash, ok := args[0].(string) + if !ok || 0 == len(hash) { + return fmt.Errorf("arg[0] is not a string, got %v", args[0]), false + } + if err := geohash.Validate(hash); nil != err { + return err, false + } + la, lo := geohash.Decode(hash) + return position{Longitude: lo, Latitude: la}, true +} + +func (r *geohashDecodeInt) Exec(args []any, _ api.FunctionContext) (any, bool) { + hash, ok := args[0].(uint64) + if !ok { + return fmt.Errorf("arg[0] is not a bigint, got %v", args[0]), false + } + la, lo := geohash.DecodeInt(hash) + return position{Longitude: lo, Latitude: la}, true +} + +func (r *geohashBoundingBox) Exec(args []any, _ api.FunctionContext) (any, bool) { + hash, ok := args[0].(string) + if !ok || 0 == len(hash) { + return fmt.Errorf("arg[0] is not a string, got %v", args[0]), false + } + if err := geohash.Validate(hash); nil != err { + return err, false + } + return geohash.BoundingBox(hash), true +} + +func (r *geohashBoundingBoxInt) Exec(args []any, _ api.FunctionContext) (any, bool) { + hash, ok := args[0].(uint64) + if !ok { + return fmt.Errorf("arg[0] is not a bigint, got %v", args[0]), false + } + return geohash.BoundingBoxInt(hash), true +} + +func (r *geohashNeighbor) Exec(args []any, _ api.FunctionContext) (any, bool) { + hash, ok := args[0].(string) + if !ok || 0 == len(hash) { + return fmt.Errorf("arg[0] is not a string, got %v", args[0]), false + } + if err := geohash.Validate(hash); nil != err { + return err, false + } + var directionCode geohash.Direction + direction, ok := args[1].(string) + if !ok || 0 == len(direction) { + return fmt.Errorf("arg[1] is not a string, got %v", args[1]), false + } else { + directionCode, ok = g_direction[direction] + if !ok { + return fmt.Errorf("arg[1] is valid, got %v", args[1]), false + } + + } + return geohash.Neighbor(hash, directionCode), true +} + +func (r *geohashNeighborInt) Exec(args []any, _ api.FunctionContext) (any, bool) { + hash, ok := args[0].(uint64) + if !ok { + return fmt.Errorf("arg[0] is not a bigint, got %v", args[0]), false + } + var directionCode geohash.Direction + direction, ok := args[1].(string) + if !ok || 0 == len(direction) { + return fmt.Errorf("arg[1] is not a string, got %v", args[1]), false + } else { + directionCode, ok = g_direction[direction] + if !ok { + return fmt.Errorf("arg[1] is valid, got %v", args[1]), false + } + } + return geohash.NeighborInt(hash, directionCode), true +} + +func (r *geohashNeighbors) Exec(args []any, _ api.FunctionContext) (any, bool) { + hash, ok := args[0].(string) + if !ok || 0 == len(hash) { + return fmt.Errorf("arg[0] is not a string, got %v", args[0]), false + } + if err := geohash.Validate(hash); nil != err { + return err, false + } + return geohash.Neighbors(hash), true +} + +func (r *geohashNeighborsInt) Exec(args []any, _ api.FunctionContext) (any, bool) { + hash, ok := args[0].(uint64) + if !ok { + return fmt.Errorf("arg[0] is not a bigint, got %v", args[0]), false + } + return geohash.NeighborsInt(hash), true +} diff --git a/extensions/functions/geohash/geohash.json b/extensions/functions/geohash/geohash.json new file mode 100644 index 0000000000..1b96de1d7e --- /dev/null +++ b/extensions/functions/geohash/geohash.json @@ -0,0 +1,479 @@ +{ + "about": { + "trial": false, + "author": { + "name": "EMQ", + "email": "contact@emqx.io", + "company": "EMQ Technologies Co., Ltd", + "website": "https://www.emqx.io" + }, + "helpUrl": { + "en_US": "https://ekuiper.org/docs/en/latest/sqls/functions/custom_functions.html", + "zh_CN": "https://ekuiper.org/docs/zh/latest/sqls/functions/custom_functions.html" + }, + "description": { + "en_US": "", + "zh_CN": "" + } + }, + "libs": [ + "github.com/mmcloughlin/geohash@master" + ], + "name": "geohash", + "functions": [ + { + "name": "geohashEncode", + "example": "geohashEncode(la,lo)", + "hint": { + "en_US": "Encode latitude and longitude as characters", + "zh_CN": "将经纬度编码为字符" + }, + "args": [ + { + "name": "latitude", + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input latitude", + "zh_CN": "输入经度" + }, + "label": { + "en_US": "Latitude", + "zh_CN": "经度" + } + }, + { + "name": "longitude", + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input longitude", + "zh_CN": "输入纬度" + }, + "label": { + "en_US": "Longitude", + "zh_CN": "输入纬度" + } + } + ], + "return": { + "type": "int", + "hint": { + "en_US": "Encoded string", + "zh_CN": "编码字符" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Geohash Encode", + "zh_CN": "Geohash 编码" + } + } + }, + { + "name": "geohashEncodeInt", + "example": "geohashEncodeInt(la,lo )", + "hint": { + "en_US": "Encode latitude and longitude as numbers", + "zh_CN": "将经纬度编码为数字" + }, + "args": [ + { + "name": "latitude", + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input latitude", + "zh_CN": "输入经度" + }, + "label": { + "en_US": "Latitude", + "zh_CN": "经度" + } + }, + { + "name": "longitude", + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input longitude", + "zh_CN": "输入纬度" + }, + "label": { + "en_US": "Longitude", + "zh_CN": "输入纬度" + } + } + ], + "return": { + "type": "int", + "hint": { + "en_US": "Encoded value", + "zh_CN": "编码值" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Geohash Encode to Integer", + "zh_CN": "Geohash 整数编码" + } + } + }, + { + "name": "geohashDecode", + "example": "geohashDecode(hash )", + "hint": { + "en_US": "Decode characters into latitude and longitude", + "zh_CN": "将字符解码为经纬度" + }, + "args": [ + { + "name": "data", + "hidden": false, + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input data", + "zh_CN": "输入数据" + }, + "label": { + "en_US": "Field", + "zh_CN": "输入数据" + } + } + ], + "return": { + "type": "struct", + "hint": { + "en_US": "Decoded Value", + "zh_CN": "解码值" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Geohash Decode", + "zh_CN": "Geohash 解码" + } + } + }, + { + "name": "geohashDecodeInt", + "example": "geohashDecodeInt(hash)", + "hint": { + "en_US": "Decode numbers into latitude and longitude", + "zh_CN": "将数字解码为经纬度" + }, + "args": [ + { + "name": "data", + "hidden": false, + "optional": false, + "control": "field", + "type": "number", + "hint": { + "en_US": "Input data", + "zh_CN": "输入数据" + }, + "label": { + "en_US": "Data", + "zh_CN": "输入数据" + } + } + ], + "return": { + "type": "struct", + "hint": { + "en_US": "Decoded Value", + "zh_CN": "解码值" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Geohash Decode Integer", + "zh_CN": "Geohash 整数解码" + } + } + }, + { + "name": "geohashBoundingBox", + "example": "geohashBoundingBox(hash )", + "hint": { + "en_US": "Area for calculating character codes", + "zh_CN": "计算字符编码的区域" + }, + "args": [ + { + "name": "data", + "hidden": false, + "optional": false, + "control": "text", + "type": "string", + "hint": { + "en_US": "Input data", + "zh_CN": "输入数据" + }, + "label": { + "en_US": "Data", + "zh_CN": "输入数据" + } + } + ], + "return": { + "type": "struct", + "hint": { + "en_US": "Box", + "zh_CN": "区域" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Geohash Bounding Box", + "zh_CN": "Geohash 边界框" + } + } + }, + { + "name": "geohashBoundingBoxInt", + "example": "geohashBoundingBoxInt(hash)", + "hint": { + "en_US": "Calculate the area of digital coding", + "zh_CN": "计算数字编码的区域" + }, + "args": [ + { + "name": "data", + "hidden": false, + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input data", + "zh_CN": "输入数据" + }, + "label": { + "en_US": "Data", + "zh_CN": "输入数据" + } + } + ], + "return": { + "type": "int", + "hint": { + "en_US": "Bounding Box", + "zh_CN": "区域编码" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Geohash Integer Bounding Box", + "zh_CN": "Geohash 整数边界框" + } + } + }, + { + "name": "geohashNeighbor", + "example": "geohashNeighbor(hash,direction )", + "hint": { + "en_US": "Calculate the neighbor of the corresponding direction of the character encoding", + "zh_CN": "计算字符编码对应方向的邻居" + }, + "args": [ + { + "name": "hash", + "hidden": false, + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input data", + "zh_CN": "输入数据" + }, + "label": { + "en_US": "Field", + "zh_CN": "输入数据" + } + }, + { + "name": "direction", + "optional": false, + "control": "text", + "type": "string", + "hint": { + "en_US": "Input direction", + "zh_CN": "输入方向" + }, + "label": { + "en_US": "Direction", + "zh_CN": "方向" + } + } + ], + "return": { + "type": "string", + "hint": { + "en_US": "Neighbor", + "zh_CN": "相邻位置" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Geohash Neighbor", + "zh_CN": "Geohash 相邻位置" + } + } + }, + { + "name": "geohashNeighborInt", + "example": "geohashNeighborInt(hash,direction )", + "hint": { + "en_US": "Calculate the neighbors in the corresponding direction of the digital code", + "zh_CN": "计算数字编码对应方向的邻居" + }, + "args": [ + { + "name": "hash", + "hidden": false, + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input data", + "zh_CN": "输入数据" + }, + "label": { + "en_US": "Data", + "zh_CN": "输入数据" + } + }, + { + "name": "direction", + "optional": false, + "control": "text", + "type": "string", + "hint": { + "en_US": "Input direction", + "zh_CN": "输入方向" + }, + "label": { + "en_US": "Direction", + "zh_CN": "方向" + } + } + ], + "return": { + "type": "string", + "hint": { + "en_US": "Neighbor", + "zh_CN": "相邻位置编码" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Geohash Neighbor Integer", + "zh_CN": "Geohash 相邻位置编码" + } + } + }, + { + "name": "geohashNeighbors", + "example": "geohashNeighbors(hash)", + "hint": { + "en_US": "Calculate all neighbors of character encoding", + "zh_CN": "计算字符编码的所有邻居" + }, + "args": [ + { + "name": "hash", + "hidden": false, + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input data", + "zh_CN": "输入数据" + }, + "label": { + "en_US": "Field", + "zh_CN": "输入数据" + } + } + ], + "return": { + "type": "array", + "hint": { + "en_US": "All Neighbor", + "zh_CN": "所有相邻位置" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Geohash All Neighbors", + "zh_CN": "Geohash 所有相邻位置" + } + } + }, + { + "name": "geohashNeighborsInt", + "example": "geohashNeighborsInt(hash)", + "hint": { + "en_US": "Calculate all neighbors of digital encoding", + "zh_CN": "计算数字编码的所有邻居" + }, + "args": [ + { + "name": "hash", + "hidden": false, + "optional": false, + "control": "text", + "type": "string", + "hint": { + "en_US": "Input data", + "zh_CN": "输入数据" + }, + "label": { + "en_US": "Field", + "zh_CN": "输入数据" + } + } + ], + "return": { + "type": "array", + "hint": { + "en_US": "All Neighbor", + "zh_CN": "所有相邻位置编码" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Geohash All Neighbors Integer", + "zh_CN": "Geohash 所有相邻位置编码" + } + } + } + ] +} diff --git a/extensions/functions/image/exports.go b/extensions/functions/image/exports.go new file mode 100644 index 0000000000..4e545fa54d --- /dev/null +++ b/extensions/functions/image/exports.go @@ -0,0 +1,20 @@ +// Copyright 2021-2024 EMQ Technologies Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +var ( + Thumbnail thumbnail + ResizeWithChan imageResize +) diff --git a/extensions/functions/image/image.json b/extensions/functions/image/image.json new file mode 100644 index 0000000000..7c9b33ce78 --- /dev/null +++ b/extensions/functions/image/image.json @@ -0,0 +1,175 @@ +{ + "about": { + "trial": false, + "author": { + "name": "EMQ", + "email": "contact@emqx.io", + "company": "EMQ Technologies Co., Ltd", + "website": "https://www.emqx.io" + }, + "helpUrl": { + "en_US": "https://ekuiper.org/docs/en/latest/sqls/functions/custom_functions.html", + "zh_CN": "https://ekuiper.org/docs/zh/latest/sqls/functions/custom_functions.html" + }, + "description": { + "en_US": "", + "zh_CN": "" + } + }, + "libs": [ + "github.com/nfnt/resize@master" + ], + "name": "image", + "functions": [ + { + "name": "resize", + "example": "resize(image,width, height, isRaw)", + "hint": { + "en_US": "Creates a scaled image with new dimensions (width, height) .If either width or height is set to 0, it will be set to an aspect ratio preserving value.", + "zh_CN": "创建具有新尺寸(宽度,高度)的缩放图像。如果width或height设置为0,则将其设置为长宽比保留值。" + }, + "args": [ + { + "name": "image", + "hidden": false, + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input image", + "zh_CN": "输入图像" + }, + "label": { + "en_US": "Image", + "zh_CN": "图像" + } + }, + { + "name": "width", + "optional": false, + "control": "text", + "type": "int", + "hint": { + "en_US": "Input width", + "zh_CN": "输入宽度" + }, + "label": { + "en_US": "Width", + "zh_CN": "宽度" + } + }, + { + "name": "height", + "optional": false, + "control": "text", + "type": "int", + "hint": { + "en_US": "Input height", + "zh_CN": "输入高度" + }, + "label": { + "en_US": "Height", + "zh_CN": "高度" + } + }, + { + "name": "isRaw", + "optional": true, + "control": "radio", + "type": "bool", + "hint": { + "en_US": "Whether to output raw data, set to true when using by most AI inference", + "zh_CN": "输出未编码原始数据,通常 AI 推断需要原始数据" + }, + "label": { + "en_US": "Output Raw Data", + "zh_CN": "输出原始数据" + } + } + ], + "return": { + "type": "bytea", + "hint": { + "en_US": "Resized image", + "zh_CN": "缩放后的图像" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Image Resize", + "zh_CN": "图像调整大小" + } + } + }, + { + "name": "thumbnail", + "example": "thumbnail(image,maxWidth, maxHeight)", + "hint": { + "en_US": "Downscales an image preserving its aspect ratio to the maximum dimensions (maxWidth, maxHeight).", + "zh_CN": "将保留宽高比的图像缩小到最大尺寸(maxWidth,maxHeight)。" + }, + "args": [ + { + "name": "image", + "hidden": false, + "optional": false, + "control": "field", + "type": "string", + "hint": { + "en_US": "Input image", + "zh_CN": "输入图像" + }, + "label": { + "en_US": "Image", + "zh_CN": "图像" + } + }, + { + "name": "maxWidth", + "optional": false, + "control": "text", + "type": "int", + "hint": { + "en_US": "Input maxWidth", + "zh_CN": "输入最大宽度" + }, + "label": { + "en_US": "Max Width", + "zh_CN": "最大宽度" + } + }, + { + "name": "maxHeight", + "optional": false, + "control": "text", + "type": "int", + "hint": { + "en_US": "Input maxHeight", + "zh_CN": "输入最大高度" + }, + "label": { + "en_US": "MaxHeight", + "zh_CN": "最大高度" + } + } + ], + "return": { + "type": "bytea", + "hint": { + "en_US": "Thumbnail", + "zh_CN": "缩略图" + } + }, + "node": { + "category": "function", + "icon": "iconPath", + "label": { + "en_US": "Thumbnail", + "zh_CN": "缩略图" + } + } + } + ] +} diff --git a/extensions/functions/image/img.png b/extensions/functions/image/img.png new file mode 100644 index 0000000000000000000000000000000000000000..d49afe27c115a8d082f658ca8d3e72c7b193f82f GIT binary patch literal 47410 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
I1+f#=j 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 G+y4PWI*8r? literal 0 HcmV?d00001 diff --git a/extensions/functions/image/resize.go b/extensions/functions/image/resize.go new file mode 100644 index 0000000000..d968735687 --- /dev/null +++ b/extensions/functions/image/resize.go @@ -0,0 +1,103 @@ +// Copyright 2021-2024 EMQ Technologies Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "bytes" + "fmt" + "image" + "image/gif" + "image/jpeg" + "image/png" + + "github.com/nfnt/resize" + + "github.com/lf-edge/ekuiper/contract/v2/api" +) + +type imageResize struct{} + +func (f *imageResize) Validate(args []any) error { + if len(args) < 3 { + return fmt.Errorf("The resize function must have at least 3 parameters, but got %d", len(args)) + } + return nil +} + +func (f *imageResize) IsAggregate() bool { + return false +} + +func (f *imageResize) Exec(args []any, ctx api.FunctionContext) (any, bool) { + arg, ok := args[0].([]byte) + if !ok { + return fmt.Errorf("arg[0] is not a bytea, got %v", args[0]), false + } + width, ok := args[1].(int) + if !ok || 0 > width { + return fmt.Errorf("arg[1] is not a bigint, got %v", args[1]), false + } + height, ok := args[2].(int) + if !ok || 0 > height { + return fmt.Errorf("arg[2] is not a bigint, got %v", args[2]), false + } + isRaw := false + if len(args) > 3 { + isRaw, ok = args[3].(bool) + if !ok { + return fmt.Errorf("arg[3] is not a bool, got %v", args[3]), false + } + } + ctx.GetLogger().Debugf("resize: %d %d, output raw %v", width, height, isRaw) + + img, format, err := image.Decode(bytes.NewReader(arg)) + if nil != err { + return fmt.Errorf("image decode error:%v", err), false + } + + img = resize.Resize(uint(width), uint(height), img, resize.Bilinear) + if isRaw { + bounds := img.Bounds() + dx, dy := bounds.Dx(), bounds.Dy() + bb := make([]byte, width*height*3) + for y := 0; y < dy; y++ { + for x := 0; x < dx; x++ { + col := img.At(x, y) + r, g, b, _ := col.RGBA() + bb[(y*dx+x)*3+0] = byte(float64(r) / 255.0) + bb[(y*dx+x)*3+1] = byte(float64(g) / 255.0) + bb[(y*dx+x)*3+2] = byte(float64(b) / 255.0) + } + } + return bb, true + } else { + var b []byte + buf := bytes.NewBuffer(b) + switch format { + case "png": + err = png.Encode(buf, img) + case "jpeg": + err = jpeg.Encode(buf, img, nil) + case "gif": + err = gif.Encode(buf, img, nil) + default: + return fmt.Errorf("%s image type is not currently supported", format), false + } + if nil != err { + return fmt.Errorf("image encode error:%v", err), false + } + return buf.Bytes(), true + } +} diff --git a/extensions/functions/image/resize_test.go b/extensions/functions/image/resize_test.go new file mode 100644 index 0000000000..b850f1409c --- /dev/null +++ b/extensions/functions/image/resize_test.go @@ -0,0 +1,95 @@ +// Copyright 2022-2024 EMQ Technologies Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + kctx "github.com/lf-edge/ekuiper/v2/internal/topo/context" + mockContext "github.com/lf-edge/ekuiper/v2/pkg/mock/context" +) + +func TestResize(t *testing.T) { + ctx := mockContext.NewMockContext("testResize", "p[") + fctx := kctx.NewDefaultFuncContext(ctx, 2) + err := ResizeWithChan.Validate([]any{}) + assert.EqualError(t, err, "The resize function must have at least 3 parameters, but got 0") + isAgg := ResizeWithChan.IsAggregate() + assert.False(t, isAgg) + payload, err := os.ReadFile("img.png") + assert.NoError(t, err) + resized, err := os.ReadFile("resized.png") + assert.NoError(t, err) + tests := []struct { + n string + args []any + e string + r []byte + }{ + { + n: "normal", + args: []any{payload, 100, 100}, + }, + { + n: "wrong payload", + args: []any{"img.png", 100, 100}, + e: "arg[0] is not a bytea, got img.png", + }, + { + n: "wrong width", + args: []any{payload, "100", 100}, + e: "arg[1] is not a bigint, got 100", + }, + { + n: "wrong height", + args: []any{payload, 100, "100"}, + e: "arg[2] is not a bigint, got 100", + }, + { + n: "wrong raw", + args: []any{payload, 100, 100, 1}, + e: "arg[3] is not a bool, got 1", + }, + { + n: "not image", + args: []any{[]byte{0x1, 0x2}, 100, 100, false}, + e: "image decode error:image: unknown format", + }, + { + n: "raw", + args: []any{payload, 4, 4, true}, + r: []byte{0x3c, 0x40, 0x4b, 0x39, 0x3e, 0x4a, 0x39, 0x3e, 0x4a, 0x38, 0x3d, 0x47, 0x36, 0x3a, 0x44, 0x35, 0x39, 0x44, 0x35, 0x39, 0x44, 0x35, 0x39, 0x43, 0x3a, 0x3e, 0x47, 0x3a, 0x3e, 0x49, 0x3a, 0x3d, 0x48, 0x38, 0x3c, 0x46, 0x33, 0x37, 0x40, 0x35, 0x3a, 0x43, 0x37, 0x40, 0x46, 0x32, 0x36, 0x3f}, + }, + } + for _, tt := range tests { + t.Run(tt.n, func(t *testing.T) { + result, success := ResizeWithChan.Exec(tt.args, fctx) + if tt.e == "" { + assert.True(t, success) + if tt.r != nil { + assert.Equal(t, tt.r, result) + } else { + assert.Equal(t, resized, result) + } + } else { + assert.EqualError(t, result.(error), tt.e) + } + }) + + } +} diff --git a/extensions/functions/image/resized.png b/extensions/functions/image/resized.png new file mode 100644 index 0000000000000000000000000000000000000000..a237876e783bc937791f96adc22101e917f8145e GIT binary patch literal 3158 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 w|Lw9J!s><1P1vt}#CjD#>L*+O8vp?R|F* maxWidth { + return fmt.Errorf("arg[1] is not a bigint, got %v", args[1]), false + } + maxHeight, ok := args[2].(int) + if !ok || 0 > maxHeight { + return fmt.Errorf("arg[2] is not a bigint, got %v", args[2]), false + } + img, format, err := image.Decode(bytes.NewReader(arg)) + if nil != err { + return fmt.Errorf("image decode error:%v", err), false + } + img = resize.Thumbnail(uint(maxWidth), uint(maxHeight), img, resize.Bilinear) + + var b []byte + buf := bytes.NewBuffer(b) + switch format { + case "png": + err = png.Encode(buf, img) + case "jpeg": + err = jpeg.Encode(buf, img, nil) + default: + return fmt.Errorf("%s image type is not currently supported", format), false + } + if nil != err { + return fmt.Errorf("image encode error:%v", err), false + } + return buf.Bytes(), true +} + +func (f *thumbnail) IsAggregate() bool { + return false +} diff --git a/extensions/functions/image/thumnail_test.go b/extensions/functions/image/thumnail_test.go new file mode 100644 index 0000000000..dd6e4fc41a --- /dev/null +++ b/extensions/functions/image/thumnail_test.go @@ -0,0 +1,81 @@ +// Copyright 2024 EMQ Technologies Co., Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package main + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + kctx "github.com/lf-edge/ekuiper/v2/internal/topo/context" + mockContext "github.com/lf-edge/ekuiper/v2/pkg/mock/context" +) + +func TestThumbnail(t *testing.T) { + ctx := mockContext.NewMockContext("testResize", "p[") + fctx := kctx.NewDefaultFuncContext(ctx, 2) + err := Thumbnail.Validate([]any{}) + assert.EqualError(t, err, "The thumbnail function supports 3 parameters, but got 0") + isAgg := Thumbnail.IsAggregate() + assert.False(t, isAgg) + payload, err := os.ReadFile("img.png") + assert.NoError(t, err) + tests := []struct { + n string + args []any + e string + r []byte + }{ + { + n: "normal", + args: []any{payload, 100, 100}, + }, + { + n: "wrong payload", + args: []any{"img.png", 100, 100}, + e: "arg[0] is not a bytea, got img.png", + }, + { + n: "wrong width", + args: []any{payload, "100", 100}, + e: "arg[1] is not a bigint, got 100", + }, + { + n: "wrong height", + args: []any{payload, 100, "100"}, + e: "arg[2] is not a bigint, got 100", + }, + { + n: "not image", + args: []any{[]byte{0x1, 0x2}, 100, 100, false}, + e: "image decode error:image: unknown format", + }, + } + for _, tt := range tests { + t.Run(tt.n, func(t *testing.T) { + result, success := Thumbnail.Exec(tt.args, fctx) + if tt.e == "" { + assert.True(t, success) + if tt.r != nil { + assert.Equal(t, tt.r, result) + } + } else { + assert.EqualError(t, result.(error), tt.e) + } + }) + + } +} diff --git a/go.mod b/go.mod index acb935e7cb..fcbe47d572 100644 --- a/go.mod +++ b/go.mod @@ -33,6 +33,7 @@ require ( github.com/lf-edge/ekuiper/contract/v2 v2.0.0-alpha.3 github.com/mattn/go-tflite v1.0.5 github.com/mitchellh/mapstructure v1.5.0 + github.com/mmcloughlin/geohash v0.10.0 github.com/mochi-mqtt/server/v2 v2.6.4 github.com/modern-go/reflect2 v1.0.2 github.com/montanaflynn/stats v0.7.1 diff --git a/go.sum b/go.sum index 2178bf9d2a..517cbf2696 100644 --- a/go.sum +++ b/go.sum @@ -376,6 +376,8 @@ github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RR github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/mmcloughlin/geohash v0.10.0 h1:9w1HchfDfdeLc+jFEf/04D27KP7E2QmpDu52wPbJWRE= +github.com/mmcloughlin/geohash v0.10.0/go.mod h1:oNZxQo5yWJh0eMQEP/8hwQuVx9Z9tjwFUqcTB1SmG0c= github.com/mochi-mqtt/server/v2 v2.6.4 h1:zuKokG/YzmefLecpodu1VSOSXJf1GP9mk2LdVcp1Jp4= github.com/mochi-mqtt/server/v2 v2.6.4/go.mod h1:TqztjKGO0/ArOjJt9x9idk0kqPT3CVN8Pb+l+PS5Gdo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=