马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。
您需要 登录 才可以下载或查看,没有账号?站点注册
×
PBIDEA:用 uo_image 做图片处理与二维码条码全解(格式转换、缩放旋转、抠图、生成与回读,PB10 基准 · PB12.5 实测)
【版本声明】
- 本文以 PBIDEA 当前最新版本为准(版本源由维护方统一核验),并对照随版发布的源码整理。
- 核验日期:2026-09-25;当次核验的 PBIDEA 核心运行库:18,017,792 字节 / 2544 个导出,md5 048e3e92d4a4…。
- 版本识别口径:逐项解析 PE 导出表(文件体积、目录名都不能作为判据)。
- 适用平台:Windows XP 及以上(XP / 7 / 10 / 11 均可);PowerBuilder 8 及以上(PB 8/9/10/10.5/11.x/12.x 及以上通用)。本文的实机验证环境为 Windows 10 + PB 10 与 PB 12.5。
- PBIDEA 版本会持续更新,本文只对上述核验时点的最新版本负责;后续若行为有变,以新版为准。
阅读说明
1. 适用版本:PowerBuilder 10(写作基准)及以上。文中全部代码在 PB10 与 PB12.5 双轨上都做过「编译器全量重建 + 虚拟机运行断言」两件事,两版输出逐字节一致;PB12.5 的差异单独列在第十二节。
2. 支持数据库:本篇是纯图像/编码处理,不涉及数据库,不需要任何 DBMS 连接。
3. 操作系统与环境要求:Windows XP 及以上;PowerBuilder 8 及以上;需要 PBIDEA 运行库所在目录可写(生成图片要落盘),并把 PBIDEA 的 PBL 加进应用库列表、运行库 DLL 放到可执行文件旁;源码用 GBK + CRLF、不带 BOM。
4. 难度系数:★★★☆☆(入门容易,但三处重载解析与「画布前置条件」是硬坑,踩中会浪费一整天)。
5. 其它说明:本文所有结论都来自真机运行读数,不是读文档推断。凡我实测到与直觉相反的地方,都在第十节「坑清单」里逐条标出来,并给出规避写法。
一、uo_image 到底是什么
PBIDEA 的 websuite.pbl 里有一批 uo_* 非可视对象,uo_image 是其中负责图像的那一个。它是 PbIdea.dll 的薄封装——方法体基本都是一行 system library "PbIdea.dll" alias for "imageXxx"。
理解它只要抓住一句话:
uo_image 实例内部持有一张「画布」(运行库内部叫 pImage)。所有几何变换、颜色处理、条码绘制,作用的都是这张画布;Save 是把画布写盘。
这句话解释了后面几乎所有"怪现象":
- 变换类方法(Zoom / Rotate / Flip / TransParentColor…)没有画布时无处落笔,直接失败;
- GetSize 读的是画布尺寸,不是文件尺寸;
- OpenBarCode 是往画布上画条码,不是"凭空造一张条码图"——这就是第十节第 1 号大坑的根因。
标准写法:
- // 声明与创建
- uo_image limg
- boolean lb_ok
- limg = create uo_image // 创建实例(此时还没有画布)
- lb_ok = limg.Open('demo.png') // 这一步才产生画布
- // ... 中间做变换
- lb_ok = limg.Save('out.png') // 把画布写盘
- limg.Close() // 释放画布
- destroy limg // 释放实例
复制代码
uo_image 的 destructor 事件里已经写了一行 Close(),所以即使忘了显式 Close 也不会泄漏;但显式调用能让生命周期更清楚,尤其在循环里反复处理图片时。
二、API 全貌
把 uo_image.sru 的声明段摊开,公开方法一共 30 多个,按用途分五类:
| 分类 | 方法 | 重载数 | 说明 | | 创建/释放 | Open | 4 | (string) / (string,int) / (blob) / (blob,int);后一个是 paintMode | | Close | 1 | 释放画布;幂等,未开图也能调 | | 读写文件 | Save | 2 | (string fileName) / (string imageFormat, ref blob image) | | MakeQrcode | 4 | 直接生成二维码文件,不依赖画布 | | 尺寸 | GetSize | 1 | (ref int w, ref int h) | | SetSize | 1 | (int w, int h),凭空建一张画布 | | Zoom | 2 | (int w,int h) 固定尺寸 / (double x,double y) 按倍率 | | 几何 | Rotate | 2 | (int nAngle) 实际角度 / (int nAngle,int method) 固定角度模式 | | Flip / Mirror | 各 1 | 垂直翻转 / 水平镜像 | | 颜色 | ReplaceColor | 4 | 换色,带容差 | | TransParentColor | 1 | (ulong clrSrc, double quotient) 把某色抠成透明 | | SetAlpha | 1 | (int nAlpha) 1–255 | | SetAlphaColor | 1 | (ulong color) 把某色整体设为透明 | | SetMaskColor | 1 | (ulong colorMask) 蒙版色 | | 编码 | OpenQrCode | 3 | 生成二维码画布:6 / 7 / 8 参 | | OpenBarCode | 2 | 生成条码:5 参字符串版 / (uo_json) | | DecodeSymbol | 2 | (string fileName, uo_json result) / (blob, uo_json) | | CreateMultiBarCode | 1 | (uo_json) → blob,一次画多条码 | | 界面 | Paint | 3 | 把画布画到某个可视对象上 | | BindPicture / UnBindPicture | 各 1 | 绑定/解绑 Picture 控件 | | SetAnimate | 1 | (int nMs, int nMode, int nStep) 动画 | | InitUserObject | 1 | (ulong hwnd) |
记住两个"没有":没有 SetXData/SetYData 之类的绘图 API(那是别的组件),也没有任何"画线、画圆"的图元函数。uo_image 的定位是位图搬运工 + 编码器,不是画板。
三、加载与输出:Open / Save / GetSize / SetSize
3.1 能打开什么
| 输入 | Open | GetSize | | BMP / PNG / JPG / JPEG | T | 正常宽高 | | SVG | T | 正常(实测取到 48×48) | | GIF | T | 0×0(能打开但读不到尺寸) | | 不存在的文件 / 非图片文件 / 空字符串 | F | F + 0×0 | | PNG 的 base64 文本(含 data:image/png;base64, 前缀) | T | 正常宽高 |
最后一行值得单独说:Open 的重载名叫 fileNameorBase64,也就是说可以直接把 base64 字符串当"文件名"传进去,带不带 data: URI 前缀都认。对接前端上传、或从数据库 blob 转出来时很省事。
Open(blob) 重载则用来接二进制:Save('.bmp', ref lblob) 把画布导成 blob,再 Open(lblob) 就能读回来——实测此路通畅,blob 长度与落盘文件字节数完全相等。
3.2 能存成什么
Save 的格式完全由文件扩展名决定,实测支持面:
| 扩展名 | 结果 | | .bmp | ✅ | | .png | ✅ | | .jpg | ✅ | | .jpeg | ✅(与 .jpg 等价,字节数相同) | | .gif / .webp / .tiff / 无扩展名 | ❌ 返回 false |
想输出 JPEG 质量、PNG 压缩级别这类参数?没有对应入口。Save 只吃一个路径,格式参数不可调。要精细控制,只能离开 uo_image 这条链。
3.3 Save 返回值不能当真
这是本文第二个必须记住的坑:
- lb = limg.Save('no_such_dir/out.png') // 返回 TRUE
复制代码
返回 true 不代表文件写出来了。 实测把一个不存在的目录当目标时,Save 照样返回 true——有意思的是,那个缺失的目录确实被创建了,但文件不在里面,FileLength 返回 -1。
所以落盘之后必须复核,我习惯加一个小工具函数:
- // 取文件字节数;文件不存在返回 -1
- public function string of_exists (string as_file);
- if FileExists(as_file) then
- return String(FileLength(as_file))
- end if
- return '-1'
- end function
复制代码
调用方这样用:
- lb = limg.Save(as_dst)
- ls_out = ls_out + '另存:' + String(lb) + ' ' + of_exists(as_dst) + ' 字节'
复制代码
判决依据是字节数,不是返回值。 这一条能帮你省掉"明明返回成功、用户却看不到图"的排查时间。
3.4 SetSize:凭空造一张画布
- limg = create uo_image
- lb = limg.SetSize(400, 60) // 建一张 400×60 的画布
- lb = limg.GetSize(lw, lh) // → 400 x 60
复制代码
SetSize 不打开任何文件就建出画布,颜色默认白底。它是后面第七节条码能生成成功的前置条件。
四、几何变换:三个重载陷阱
4.1 Zoom:字面量会掉进 double 重载 ★★★
Zoom 有两个重载:
- function boolean Zoom(int nWidth, int nHeight)
- function boolean Zoom(readonly double xZoom, readonly double yZoom)
复制代码
看起来清清楚楚,但实测:
- limg.Zoom(32, 24) // 期望 32x24 —— 实际得到 2048 x 1152 !
复制代码
因为 32 和 24 是整型字面量,PowerBuilder 的重载解析把 Zoom(32,24) 匹配到了 double 版本,于是 32 被当成"横向放大 32 倍"、24 当成"纵向放大 24 倍",64×48 的图被拉成 2048×1152。
正确写法——参数必须是 integer 变量:
- integer lw2
- integer lh2
- lw2 = 32
- lh2 = 24
- limg.Zoom(lw2, lh2) // → 32 x 24 ✅
复制代码
uo_image.sru 的源码注释里其实警示过这一点("缩放一定要用变量定义,直接用数字会默认 double"),但这是运行时行为,编译器不会报错、不会告警,只是图悄悄变大了几十倍。做缩略图的人如果没看尺寸校验,会一直以为是"图片本身有问题"。
要做倍率缩放,反而可以直接写字面量:
- limg.Zoom(0.5, 0.5) // → 64x48 变成 32x24 ✅
- limg.Zoom(2.0, 2.0) // → 64x48 变成 128x96 ✅
复制代码
4.2 Rotate:哪一种角度会真的生效
Rotate 的两个重载是"两条不同的路":
- Rotate(nAngle) 走实际角度;
- Rotate(nAngle, 1) 走固定角度模式,此时第一参是序号而角度值,1/2/3 分别对应 0/90/180/270 度。
实测(源图 64×48,用非对称图取证避免"转了看不出来"):
| 调用 | 结果尺寸 | 是否真的转了 | | Rotate(1, 1) | 48 × 64 | ✅ 顺时针 90°(宽高互换) | | Rotate(180) | 64 × 48 | ✅ 180° | | Rotate(45) | 64 × 48 | ❌ 尺寸没变,45° 静默不生效 | | Rotate(0) | 64 × 48 | ❌ 无操作 | | Rotate(400) | 64 × 48 | ❌ 无操作 |
结论:只有 90 的整数倍会真的转。想要任意角度旋转,uo_image 做不到,它不会替你插值补边。像素级复核(自己解 PNG/BMP 逐象限采样)确认了 Rotate(1,1) 之后的象限分布确实是顺时针 90°。
4.3 Flip / Mirror
Flip() 垂直翻转、Mirror() 水平镜像,都是无参、返回 boolean,尺寸不变。像素复核确认语义正确。这两个比 Rotate 好用:语义单一、没有重载歧义。
链式调用也成立,但要注意顺序——先缩放再旋转和先旋转再缩放,结果尺寸不一样:
- limg.Open(as_src) // 64x48
- limg.Zoom(lw2, lh2) // 32x24
- limg.Rotate(1, 1) // 24x32
- limg.Save('chain.bmp')
复制代码
五、颜色与透明:名字很唬人,效果要实测
这一块是"看名字猜行为"最容易翻车的地方。
5.1 ReplaceColor:四个重载,结果一样
源码里声明了四个:
- subroutine ReplaceColor(ulong clrDst)
- subroutine ReplaceColor(ulong clrDst, ulong clrSrc)
- subroutine ReplaceColor(ulong clrDst, ulong clrSrc, readonly double quotient)
- subroutine ReplaceColor(ulong clrDst, readonly double quotient)
复制代码
第三个参数的参数名是 quotient("商/比例"),但它究竟是"容差"还是"混合比例",源码没有任何注释说明。实测把四个重载各跑一遍、对输出做像素级对比:四个重载产出的像素结果完全相同,第三个参数在这个重载组合下对结果没有影响。
实用结论:只用 ReplaceColor(clrDst) 或 ReplaceColor(clrDst, clrSrc) 就好,别指望第三个参数调节精度。
5.2 TransParentColor:真正能写出 alpha 的那个
- limg.TransParentColor(RGB(0, 255, 0), 10.0)
- limg.Save('recut.png')
复制代码
(颜色, 色差容差):容差 0 表示只抠完全相同的颜色,10.0 表示近似色一起抠。
实测效果真实写入文件:抠掉绿底后另存 PNG,文件色彩类型从 RGB 变成 RGBA(colortype 6),被抠象限的 alpha 通道值为 0。这是 uo_image 里唯一能确认"alpha 真的落进了文件"的路径。
5.3 SetAlpha / SetAlphaColor / SetMaskColor:效果与直觉不同
同样是"透明",另外三个 API 的表现就差很多:
| 调用 | 实测结果 | | SetAlpha(0) / (1) / (128) / (255) / (300) | 五种取值输出的 PNG 字节数完全相同(189 字节)→ alpha 没有写进文件 | | SetAlphaColor(RGB(255,255,255)) 后存 JPG | 与完全不调用时的字节数完全相同 | | SetMaskColor(RGB(255,0,0)) | 左上象限变成 alpha=0 的黑色,效果真实生效 |
SetAlpha 的"没写进文件"很可能是它能生效的对象是另一个输出路径(比如 Paint 到界面时),而不是 Save 到 PNG。总之:要透明效果,优先用 TransParentColor;SetAlpha 不要指望它在 PNG 里体现。
顺带一提 RGB() 的取值(写代码核对数值时用得上):
| 表达式 | 数值 | | RGB(255, 0, 0) 红 | 255 | | RGB(0, 255, 0) 绿 | 65280 | | RGB(0, 0, 255) 蓝 | 16711680 | | RGB(255,255,255) 白 | 16777215 |
六、二维码:两条路,建议走 json
uo_image 生成二维码有两条完全不同的路:
路 A:MakeQrcode —— 直接写文件,不需要画布。
- function boolean MakeQrcode(readonly string fileName, readonly string text0)
- function boolean MakeQrcode(readonly string fileName, readonly string text0, int error_level)
- function boolean MakeQrcode(readonly string fileName, readonly string text0, int error_level, int version)
- function boolean MakeQrcode(readonly uo_json json)
复制代码
路 B:OpenQrCode —— 往当前画布上生成二维码,之后要自己 Save。
- function boolean OpenQrCode(readonly string codeText, int nVersion, int nLevel,
- ulong clr, ulong clrBack, readonly string LogoPng)
- function boolean OpenQrCode(..., boolean useUTF8)
- function boolean OpenQrCode(..., boolean useUTF8, int width_height)
复制代码
6.1 实测:只有 json 形式稳定
在无人值守的运行环境里,逐个重载跑下来:
| 调用形式 | 返回 | 是否真的生成了 | | OpenQrCode 6 参 | F | ❌ 画布 0×0,Save 失败 | | OpenQrCode 7 参(带 useUTF8) | F | ❌ 同上 | | OpenQrCode 8 参(带 width_height) | F | ✅ 画布 256×256,Save 成功,DecodeSymbol 能解回原文 | | MakeQrcode(file, text) 2 参 | F | ❌ | | MakeQrcode(file, text, level) 3 参 | T | 返回 true 但文件不存在 | | MakeQrcode(file, text, level, version) 4 参 | F | ❌ | | MakeQrcode(uo_json) | T | ✅ 16,532 字节,解码回读正确 |
三个现象值得记住:
- 8 参版是"返回 F 但生成成功"——返回值不能作为判决依据,必须用 GetSize / FileLength / 解码回读来验;
- json 形式最稳,因为参数给全了;
- 上表每个用例复跑三轮读数逐字符一致,是确定性行为,不是并发竞态。
6.2 json 参数表(照抄即可)
- uo_image limg
- uo_json ljs
- boolean lb
- limg = create uo_image
- ljs = create uo_json
- ljs.Set('text', 'https://www.sybasebbs.com') // 要编码的内容
- ljs.Set('error_level', 3) // 纠错级别
- ljs.Set('version', 10) // 版本号
- ljs.Set('border', 8) // 边框(单位:模块)
- ljs.Set('space', 4) // 留白
- ljs.Set('bgcolor', RGB(255, 255, 255)) // 背景色
- ljs.Set('size', 196) // 输出像素边长
- ljs.Set('file', 'demo_qr.png') // 输出文件
- lb = limg.MakeQrcode(ljs)
复制代码
参数必须给全。 只写 text + file 两个键会静默失败(返回 false,不报错、不生成)。实测里 border / space / bgcolor / size 这几个看似可选的键,少一个就失败——具体哪个是硬要求,本文没有逐个做正交剥离,稳妥做法是整组照抄。
size 与输出的关系是直给的:size = 196,输出图片就是 196×196(实测 GetSize 与 FileLength 双确认)。想要固定尺寸的二维码,就设它。
七、条码:一个必须知道的前置条件
OpenBarCode 的参数表和 MakeQrcode 完全不同——它只有两个重载,而且没有"直接输出文件"的形式:
- function boolean OpenBarCode(readonly string codeText, string barType,
- int nHeight, double scale, readonly string options)
- function boolean OpenBarCode(readonly uo_json jsDefine)
复制代码
7.1 根因:OpenBarCode 需要"已有画布"
新实例直接调 OpenBarCode,一律失败,运行日志里是这么一句:
我把可能的前置动作逐个隔离测试(每个条件用全新的 uo_image 实例),结果如下:
| 前置动作(同一个实例上先做) | 随后 OpenBarCode(json) | 画布尺寸 | | 什么都不做 | ❌ F | 0×0 | | Open('img_in.bmp')(64×48) | ✅ T | 64×60 | | OpenQrCode(...8参)(→256×256) | ✅ T | 256×60 | | MakeQrcode(json) | ❌ F | 0×0 | | DecodeSymbol(...) | ❌ F | 0×0 | | 换个新实例(进程已被预热过) | ❌ F | 0×0 |
结论:OpenBarCode 的前提是实例内部已经有画布(由 Open / OpenQrCode / SetSize 产生)。MakeQrcode 和 DecodeSymbol 都不会造画布——它们走的是另外的路径(前者直接写文件,后者只读)。而且这个状态是按实例的,不是进程全局的(第 6 行证伪了"预热一次就够了")。
7.2 正确写法:先 SetSize,再画条码
- // 演示:生成条码(json 形式,可指定任意 zint 类型)
- // 关键前提:OpenBarCode 是「往当前画布上画条码」,
- // 实例里必须先有一张画布,否则一定失败
- // (运行时提示 not find pImage object)
- // 这里用 SetSize 建画布,再画条码、再另存
- public function string of_barcode (string as_text, string as_type, string as_file,
- integer ai_w, integer ai_h);
- uo_image limg
- uo_json ljs
- uo_json lres
- boolean lb
- integer lw
- integer lh
- string ls_out
- string ls_back
- ls_out = ''
- limg = create uo_image
- ljs = create uo_json
- lres = create uo_json
- // 第一步:建画布
- lb = limg.SetSize(ai_w, ai_h)
- // 第二步:把条码画到画布上(键名取自 uo_image 头部 zint 参数表)
- ljs.Set('text', as_text)
- ljs.Set('type', as_type)
- ljs.Set('height', ai_h)
- ljs.Set('scale', 2.0)
- lb = limg.OpenBarCode(ljs)
- ls_out = '生成条码 ' + as_type + ':' + String(lb)
- // 第三步:取画布尺寸并落盘
- lb = limg.GetSize(lw, lh)
- ls_out = ls_out + '~r~n画布尺寸:' + String(lw) + ' x ' + String(lh)
- lb = limg.Save(as_file)
- ls_out = ls_out + '~r~n条码文件:' + String(lb) + ' ' + String(of_exists(as_file)) + ' 字节'
- // 第四步:回读校验(一维条码同样能解回原始数字)
- lres.Parse('{}')
- lb = limg.DecodeSymbol(as_file, lres)
- if lb then
- lb = lres.Get('/symbol/0/data', ls_back)
- ls_out = ls_out + '~r~n条码回读:' + ls_back
- else
- ls_out = ls_out + '~r~n条码回读:未识别'
- end if
- limg.Close()
- destroy lres
- destroy ljs
- destroy limg
- return ls_out
- end function
复制代码
调用:of_barcode('6901234567892', 'EAN13', 'demo_out/demo_bar.png', 320, 60)
实测输出:生成条码 EAN13:true / 画布尺寸:320 x 60 / 条码文件:true 1433 字节 / 条码回读:6901234567892。
7.3 json 键名表
条码的 json 键名来自 zint 的符号结构,uo_image.sru 头部注释里有完整对照。常用的:
| 键 | 类型 | 说明 | | text | string | 要编码的内容(也可用数组形式 /text/0、/text/1… 编多条) | | type / symbology | number 或 string | 条码类型,字符串形式如 CODE128 / EAN13 / UPCA / PDF417 / DATAMATRIX / QRCODE | | height | number | 高度 | | scale | number | 缩放比例 | | width | number | 宽度 | | option_1 / option_2 / option_3 | number | 符号体系专属选项 | | border / border_width | number | 边框 | | cols / rows | number | 数据列数 / 行数 | | fg / bg | 6 位十六进制字符串 | 前景色 / 背景色(如 "000000" / "ffffff") | | rotate | number | 0 / 90 / 180 / 270(PNG/BMP/PCX) | | filetype | string | PNG / EPS / SVG / GIF / TXT | | output | string | 输出文件名 | | show_hrt | boolean/number | 是否显示人类可读文字 |
还有一组布尔开关:bind(加绑定条)、hidetext、show_hrt、bold(粗体文字)、box(符号周围加框)、cmyk(EPS 用 CMYK 配色)、dotty(矩阵符号用点代替方块)、dmre(允许 DataMatrix 矩形扩展)、esc(处理转义字符)、gs1、small(小号文字)、square(强制 DataMatrix 为方形)。
调用成功后,运行库会往同一个 json 对象里回写结果,实测可见 error_number(成功为 0)、error_text,以及一个 Vector 块(逐条矩形坐标)和一个 output 块(含 wmf / width / height / bitmap_width / bitmap_height)。用 /output/wmf 这种路径语法就能取出来——uo_json 的路径语法用 / 分隔、数组用数字下标,和 js.get("/text/0", ...) 是一套。
7.4 字符串重载:为什么"有时灵有时不灵"
网上(包括我之前的一些笔记)流传的说法是"OpenBarCode 的字符串重载不稳定"。这个说法要更正——它不是不稳定,是同样受画布前置条件的约束:
| 场景 | 字符串重载结果 | | 新实例直接调 | ❌ F | | 同一个实例先 Open() 过 | ✅ T | | 同一个实例先 OpenQrCode(8参) 过 | ✅ T |
所以"有时灵"的唯一变量就是前面有没有建过画布。它还和 json 版有一个行为差异:
- json 形式会按 json 里的 height 重设画布高度(320×200 的底图 → 变 320×60);
- 字符串形式不改画布尺寸,只是把条码画在现有画布上(320×200 底图 → 输出仍是 320×200,条码只占其中一块,实测暗色像素的 bbox 覆盖到第 192 行)。
7.5 画布复用会叠加,不会清空
同一个画布上连续画两次不同编码,实测是叠加而不是替换:
- limg.Open('img_big.bmp') // 320x200
- lb = limg.OpenBarCode(code128_js) // T,暗色像素 7369 个
- lb = limg.Save('d1.png') // 320x60,回读 OK
- lb = limg.OpenBarCode(datamatrix_js) // T,暗色像素涨到 8155 个
- lb = limg.Save('d2.png') // 320x60,回读 FAIL
复制代码
第二次虽然返回 true,但回读失败——上一次的图案还在,两次图案叠在一起就认不出来了。要生成多张条码,每次用 SetSize 或 Open 重新起一张画布,不要复用。
顺带说明:条码画上去之后,画布宽度决定条码能不能被扫出来。实测用 64 像素宽的画布画 EAN-13,图能存下来(708 字节)但回读失败;换成 320 或 400 像素宽就正常。做条码给宽度留足余量(一维码我一般给 300 以上)。
八、识别:DecodeSymbol 返回标准 JSON
- function boolean DecodeSymbol(readonly string fileName, readonly uo_json result)
- function boolean DecodeSymbol(readonly blob imageData, readonly uo_json result)
复制代码
识别结果写进第二个参数,结构是标准 JSON:
- {"count":1,"symbol":[{"type":"QR-Code","quality":"1","orientation":"UP",
- "data":"https://www.sybasebbs.com"}]}
复制代码
一维条码多一个 configs 字段:
- {"count":1,"symbol":[{"type":"EAN-13","quality":"433","orientation":"UP",
- "configs":"EMIT_CHECK","data":"6901234567892"}]}
复制代码
取值方式:
- ljs.Parse('{}')
- if limg.DecodeSymbol(as_file, ljs) then
- lb = ljs.Get('/symbol/0/data', ls_back) // 取第 0 个符号的 data
- end if
复制代码
注意先 Parse('{}') 把 json 清成空对象。复用一个用过的 json 对象时,不清空会把上一轮的字段留着,造成"读到了上次的值"的假象。
识别失败返回 false,count 为 0。上述读数是在同一个进程里先写后读闭环验证的:生成的二维码解回原文、生成的 EAN-13 解回数字,两条都通。
九、完整示例:一个可复用的图像处理对象
把上面所有内容打包成一个非可视对象 nvo_img_demo,对外 9 个方法:
| 方法 | 作用 | | of_demo(as_dir) | 汇总演示,一次跑完下面 7 项,返回文本报告 | | of_convert(as_src, as_dir) | 打开源图,另存 BMP / PNG / JPG 三种格式 | | of_thumbnail(as_src, as_dst, ai_w, ai_h) | 生成固定宽高的缩略图 | | of_rotate(as_src, as_dir) | 顺时针 90°、180°、垂直翻转、水平镜像 | | of_recut(as_src, as_dst) | 抠掉绿底并另存带 alpha 的 PNG | | of_qrcode(as_text, as_file, ai_size) | 生成二维码并解码回读 | | of_barcode(as_text, as_type, as_file, ai_w, ai_h) | 建画布 → 生成条码 → 落盘 → 回读 | | of_probe_size(as_file) | 只读图片尺寸 | | of_exists(as_file) | 取文件字节数,不存在返回 -1 |
界面上的调用(放在窗口某个按钮的 clicked 事件里):
- // 窗口按钮 clicked 事件:跑一遍全部演示,结果弹窗显示
- nvo_img_demo ldemo
- string ls_report
- ldemo = create nvo_img_demo
- ls_report = ldemo.of_demo('demo_out/') // 所有产物落在 demo_out 目录
- destroy ldemo
- MessageBox('uo_image 演示结果', ls_report)
复制代码
demo_out/ 目录不需要预先创建——Save 会自己把缺的目录建出来(但记住 3.3 节:建了目录不等于写进了文件)。
实测弹窗里的报告长这样(PB10 与 PB12.5 两版的输出逐字节一致):
- === 1. 格式转换 ===
- 源图:img_in.bmp
- 原始尺寸:64 x 48
- 另存 BMP:true 9270 字节
- 另存 PNG:true 148 字节
- 另存 JPG:true 787 字节
- === 2. 缩略图 ===
- 缩略图尺寸:32 x 24
- 另存:true 2358 字节
- === 3. 旋转镜像 ===
- 顺时针 90 度:48 x 64 另存=true
- 180 度:64 x 48
- 翻转/镜像:已另存 flip.bmp、mirror.bmp
- === 4. 颜色抠图 ===
- 抠绿底另存 PNG:true 190 字节(PNG 带 alpha 通道)
- === 5. 二维码 ===
- 生成二维码:true 16348 字节
- 解码回读:https://www.sybasebbs.com
- === 6. 条码 ===
- 生成条码 EAN13:true
- 画布尺寸:320 x 60
- 条码文件:true 1433 字节
- 条码回读:6901234567892
- === 7. 只读尺寸 ===
- demo_out/demo_qr.png -> 196 x 196
复制代码
把这份报告当作回归基线:以后升级 PBIDEA 版本,重跑一次,逐行对账,哪里变了立刻能看出来。
十、坑清单
| # | 坑 | 现象 | 规避 | | 1 | OpenBarCode 缺画布必败 | 返回 F,运行日志 not find pImage object | 先 SetSize(w,h) 或 Open() 建画布 | | 2 | Zoom(32,24) 命中 double 重载 | 64×48 被放大成 2048×1152 | 宽高用 integer 变量传参 | | 3 | Save 返回 true 但文件不存在 | 返回值 true、FileLength 为 -1 | 一律用 FileExists / FileLength 复核 | | 4 | MakeQrcode(json) 参数要写全 | 只给 text+file 返回 F,无提示 | 整组参数照抄 6.2 节 | | 5 | OpenQrCode 8 参返回 F 却生成成功 | 返回值误导 | 用 GetSize / 回读判成败 | | 6 | 非 90 倍数的 Rotate 静默失效 | 尺寸不变、无人报错 | 只做 90° 整数倍 | | 7 | SetAlpha 不写进 PNG | 五种取值输出字节数完全相同 | 透明用 TransParentColor | | 8 | ReplaceColor 第三参无效果 | 四重载像素结果一致 | 只用前两个重载 | | 9 | 画布复用是叠加不是替换 | 第二条条码返回 T 但回读失败 | 每次重新起画布 | | 10 | 条码画布太窄就扫不出来 | 图能存,回读失败 | 一维码画布宽给 300+ | | 11 | DecodeSymbol 前不清空 json | 读到上一轮的残留字段 | 先 Parse('{}') | | 12 | Save 不支持 gif/webp/tiff | 返回 F | 只输出 bmp/png/jpg/jpeg | | 13 | GetSize 未开图返回 F + 0×0 | 拿 0 当尺寸去算会连环错 | 先判 Open 成功 |
第十三号坑补充一个细节:Close() 是幂等的(没开过图也能调、连调两次也安全),Close() 之后 IsValid() 仍返回 true——别用 IsValid 判断画布还在不在,用 GetSize。
十一、和相近方案比一比
| 方案 | 优点 | 缺点 | 适用 | | PBIDEA uo_image | 零外部依赖;一套 API 覆盖转换/缩放/旋转/抠图/二维码/条码/识别;PB8+ 通用 | 无 JPEG 质量参数;只认 90° 整数倍旋转;部分返回值不可信 | 报表配图、二维码贴单据、条码打印、图片规格校验 | | OLE 调 WIA / GDI+ | 系统自带 | 代码冗长、PB 里易崩、跨版本差异大 | 不推荐 | | 外部命令行(ImageMagick / zint) | 功能全 | 部署要带二进制、要处理进程与权限 | 服务端批处理 | | 调用 zxing / 第三方 DLL | 识别能力强 | 额外依赖、要配路径 | 复杂识别场景 | | Excel 条码字体 | 简单 | 字体授权、扫描兼容性差 | 极简场景 |
日常需求(生成二维码贴到单据、把上传图压成缩略图、给图片打上条码)uo_image 一个组件就够,不用再引外部依赖。
十二、实机验证情况(PB10 基准 · PB12.5 实测)
红线要求:代码必须真编译、真运行。本次验证的八项读数:
| 项目 | PB10 | PB12.5 | | 源码导入(GBK / CRLF / 无 BOM) | 0 错误 | 0 错误 | | rebuild --type full 全量编译 | 0 错误 0 警告 | 0 错误(1 条既有告警,与本文代码无关) | | 运行时硬断言(.pbtest,17 条) | 17 / 17 PASS | 17 / 17 PASS | | 静态自检(0 错 0 提醒) | — | 0 错误 0 提醒 | | 编码自检(BOM/CRLF/首行头/GBK 可解码) | — | 全部 OK | | 双轨输出对账 | 651 字节,逐字节一致 | 同左 |
也就是说:PB10 和 PB12.5 跑出来的报告一字不差。
除此之外还有 10 轮隔离探针(每轮 5 到 10 组读数,累计上百条),专门用来锁定边界行为:格式支持面、重载解析、OpenBarCode 前置条件、画布复用、返回值可信度。所有"坑清单"里的条目都来自这些探针的实际读数,没有一条是文档推断。探针的进程崩溃隔离用的是"逐行 FileOpen(LineMode!, Append!) + FileWrite 落盘"的办法——即使进程被打崩,也能看到停在哪一行,排查重载和内存问题非常有效。
十三、PB12.5 差异
行为一致。 本文涉及的全部 API——Open / Save / GetSize / SetSize / Zoom / Rotate / Flip / Mirror / ReplaceColor / TransParentColor / SetAlpha / SetAlphaColor / SetMaskColor / MakeQrcode / OpenQrCode / OpenBarCode / DecodeSymbol / CreateMultiBarCode / Close——在 PB10 与 PB12.5 上的返回值、画布尺寸、输出字节数、解码结果全部一致,测试报告的 651 字节逐字节相同。
两个环境层面的差异,与代码无关,但迁移时会碰上:
- PBL 容器格式不通用。PB10 格式的应用库在 PB12.5 下会报 C0101 ... must be converted,反过来也一样。换大版本时应用库要重建,源码可以原样导入。
- 运行库要放对位置。PBIDEA 的 DLL 需要能被装载到;用无人值守方式跑(如测试虚拟机直接载入 PBL)时,工作目录下必须有这份 DLL,否则对象创建就失败。
十四、小结
uo_image 是一个"看着简单、细节密"的组件。三句话记住它:
- 它是画布式的——Open / SetSize / OpenQrCode 造画布,变换和条码绘制都作用在画布上,Save 把画布写盘;
- 它的返回值不完全可信——Save 会撒谎、OpenQrCode(8参) 会"报错但成功"、MakeQrcode(3参) 会"成功但没文件"。判决一律看尺寸、字节数和回读结果;
- 重载解析是真坑——Zoom 的字面量会掉进 double 版本,Rotate 只有 90° 整数倍真的生效。写这几个方法时,参数一律用显式类型的变量。
掌握这三点,做图片规格校验、缩略图生成、单据贴二维码、条码打印这几类需求,PBIDEA 一个组件就能落地,不用引任何外部依赖。 |