技术文章

在 Delphi 中使用 HotPDF 生成 PDF 可扫描条形码

条形码可不是拿来给你文档撑门面的破画。它是一门需要精确丈量的手艺,而扫描器就是那把用来量它的无情尺子。只要你脑子里把这层意思转过弯来,它就决定了你在往 PDF 里画条形码时几乎所有的生杀大权。那些黑杠子本身黑不溜秋的,压根不带半点信息;真正藏着情报的地方,在于那些黑杠子和它们之间白缝的宽度比例,而扫描器就是靠着激光或者传感器扫过去时,掐着表记录下这些黑白交替的耗时,来把情报给抠出来的。你要是敢把这堆线条挤成一坨、糊成一团,或者不给人家边上留够喘气的空间,那你画出来的就是一堆远看像条形码、一扫就现原形的黑泥巴。HotPDF 给你备了两条往页面上拍条形码的路子,而这俩路子之间的天壤之别,说白了就是你是想把这几何比例的生杀大权牢牢攥在自己手里,还是干脆两手一摊听天由命

一张展示了在 Delphi 中由 HotPDF 绘制的多种线性条形码符号体系网格的 PDF 页面
被 HotPDF 一把梭哈画进同一个 PDF 里的各种线性条形码符号体系

HotPDF 到底能吃下多少种编码

HotPDF 专攻的是线性(一维)符号体系,而且它能啃下来的种类,绝对比绝大多数项目能用得上的还要多。那个 THPDFBarcodeType 枚举里可是藏龙卧虎:不仅包圆了 Code 2 of 5 家族里头的交叉(interleaved)、工业(industrial)和矩阵(matrix)这些变种;还供着 Code 39 和它的加长豪华版(extended);连 Code 128 手底下的 A、B、C 三位大将也一个没落下;另外还有 Code 93 的普通版和扩展版、MSI、PostNet、Codabar;至于那些在零售店里横着走的 UPC 和 EAN 帮派,更是被一网打尽:EAN-8、EAN-13、UPC-A、被挤成干瘪老头的 UPC-E0 和 UPC-E1,外加 UPC 后头拖油瓶似的 2 位和 5 位补充码;最后还有 GS1-128(也就是 EAN-128)的那些子集。这套阵容拉出去,应付供应链标签、零售终端结账,甚至是那些还在老旧仓库里苟延残喘的工业码,那是绝对不在话下

但丑话说在前头,它绝对碰不了一丁点儿二维家族的玩意儿。这里头没有 QR 码、没有 Data Matrix、也没有 PDF417。那些个牛鬼蛇神都是自带纠错数学公式、把字节像塞肉夹馍一样塞进网格里的狠角色,如果你的需求单上明明白白写着要这几个,那对不住,你找错人了,而且这事你最好在动手砸锅卖铁之前就知道,别等搭完台子了才来哭。对于一维码来说,真正能把你逼上绝路的问题要骨感得多:你手头那些破数据,到底该往哪个符号体系的嘴里塞,因为这些个编码规矩可不是你想串门就能随便串的

这种数据上的硬规矩那是实打实的,一到生成环节就会直接跳出来咬人。像 Code 2 of 5 那些个变种还有 MSI,那是连半个非数字的字母都不肯吃的死脑筋。Code 39 稍微宽容点,能咽得下大写字母、数字外加屈指可数的几个破标点;你要是敢塞小写字母或者整个 ASCII 码表,那就乖乖去请 Code 39 扩展版或者是 Code 128 的那几个子集出山。Code 128C 为了省地盘,非得把两个数字硬生生揉进一个符号里,所以它这辈子只认偶数长度的数字串,多一个少一个都不行。EAN-13 眼巴巴地等着你喂它十二位数字,然后再自顾自地算出第十三位当校验码;EAN-8 等着吃七位自己算第八位;UPC-A 也是稳吃十二位。你要是胆敢把人家消化不了的数据硬塞进某个符号体系里,你可别指望能弹个什么贴心的报错异常出来救你,它会直接吐出一个里头装满垃圾的条形码糊弄你,这才是最要命的,因为它看着跟真的一模一样,直到收银员拿着枪对它扫了半天才发现被耍了

两条画图的道,两种捏在手里的命门

到了真刀真枪干活的时候,你应该毫不犹豫地去抱页面对象上那个 DrawBarcode 方法的大腿。这方法要吃进你要用的符号体系、一个坐标坑位、一个高度,外加一个比前头这些加起来都重要的命门参数:MUnit,也就是模块宽度(module width)。这个模块就是那根最细的黑杠子的宽度,是整个条形码里头所有其他尺寸都得管它叫爷爷的原子单位,在这儿它用的是磅(points)。这印出来的玩意儿最后到底能不能被扫出来,全看你这一步到底给它定了多大的数字

var
  Pdf: THotPDF;
begin
  Pdf := THotPDF.Create(nil);
  try
    Pdf.FileName := 'label.pdf';
    Pdf.BeginDoc;

    // BCType, X, Y, Height, MUnit (module width in points), angle,
    // data, UseCheckSum, bar color, background color.
    Pdf.CurrentPage.DrawBarcode(
      bcCodeEAN13,           // symbology
      72, 680,               // X, Y in points from the bottom-left
      60,                    // bar height
      1,                     // MUnit: 1pt narrowest bar
      0,                     // no rotation
      '123456789012',        // 12 digits; the 13th is the check
      True,                  // append the modulo-10 check digit
      clBlack, clWhite);     // bars black, background white

    Pdf.EndDoc;
  finally
    Pdf.Free;
  end;
end;

这里头有两个参数你得拿放大镜好好瞅瞅。UseCheckSum 负责在屁股后头追加那个符号体系死活都要的 modulo-10 校验位,而且对于那些个零售码来说,你几乎永远都得把它死死地按在 True 上;只有当你手里的数据早就自带了一个已经算好的校验位时,你才敢把它给关了,不然它绝对会给你下两个校验位的双黄蛋。黑杠子和底子的颜色就排在最后俩坑位,这地方可是个无数人想耍点花招最后却栽进沟里的死局,下面我们再来扒它的皮。另外,顺带再多瞅一眼那个坐标原点:跟 HotPDF 里头随便哪个画图调用一个德行,这 X 和 Y 也是从页面的左下角开始用磅尺量出来的,Y 轴更是一路往天上爬越往上越大,这套老规矩在 Hello World 示例 里早就掰开揉碎地讲过了

那第二条道叫 DirectDrawBarcode,这哥们儿的胃口是直接吞下数据外加一个包围盒(X, Y, Width, Height),然后硬生生把那堆条纹给按比例拉扯(缩放)到能正好塞满这个宽度为止。你要是想在网格上排兵布阵,这招绝对是懒人福音,因为你只要用笔画个圈,这方法就能乖乖地把杠子给填进去。但这懒省事的代价也是要命的。只要你敢给它喂一个宽度,你就等于是亲手交出了设定模块大小的生杀大权;这方法会粗暴地把你划给它的那点地盘,按着数据需要的杠子数量给强行瓜分掉,最后那根最细的杠子到底会被挤成个啥倒霉样,那就只能听天由命了。你要是敢在一个窄得连气都喘不过来的盒子里硬塞一串密密麻麻的 Code 128 进去,那些模块绝对会被悄无声息地压瘪到连天王老子的扫描器都认不出来的地步。但凡你画的码是等着真刀真枪去扫的,都给我老老实实用 DrawBarcode,然后把 MUnit 给明明白白地设死。至于 DirectDrawBarcode,那玩意儿只配拿来搞搞预览,或者是用在你已经拿尺子量过、确认最后挤出来的杠子绝对不会糊掉的那些死板排版里

模块宽度说白了就是在拿分辨率赌命

竖起耳朵听好,这笔账算的是你的标签最后到底是去是留。不管是激光枪还是摄像头扫描器,人家都有一条“再细老子就瞎了”的底线,而你那根最细的黑杠子,在印出来之后必须得舒舒服服地躺在这条底线之上。江湖上公认的、能对付一般线性码的底线,是一根 13 密耳(mil)宽的窄杠,换算下来差不多就是 0.33 毫米,很多零售和工业界的铁律都把这当成是一条绝不能踩的死线,而不是啥可以商量的目标。把这玩意儿翻译成 PDF 的黑话:1 磅等于 1/72 英寸,差不多是 0.353 毫米,也就是说,1 磅的模块宽度就刚好在这条底线上踩钢丝。这就是为什么只要你这码最后是要交到一台真家伙(扫描器)手里的,MUnit := 1 绝对是你敢报出的最低价;而如果你大发慈悲把它翻个倍设成 2,那多出来的安全感,在一个空间绰绰有余的标签上几乎跟白捡的一样便宜

现在再把这笔账跟输出分辨率挂上钩,因为这模块最后还得去闯打印机这一关。在一台 300 DPI 的激光打印机上,一个设备墨点就是 1/300 英寸,所以一个 1 磅宽的模块差不多能匀到四个墨点的身位。四个墨点勉勉强强只能凑出一个不怎么毛躁的边缘;只要碳粉稍微晕开一点,或者是套印的时候稍微打了个哆嗦,这根原本在你的 PDF 里量着正好 1 磅的杠子,印出来绝对不是胖了就是瘦了,直接把规范给踩在脚底下。要是你把模块给加码到 2 磅,你手里就能捏着八个墨点来挥霍,这点底气足够把那些印刷上的烂摊子给吃干抹净了。这是一条得刻在脑门上的铁律:你在代码里用磅定下的那个模块宽度,最后必须得能整整齐齐、舒舒服服地换算成你那台 真正用来打印的机器 分辨率下的整数个墨点,而不是拿你做梦都想买的那台高配打印机的分辨率来做梦。一个能在屏幕上扫得飞起、一出仓库打印机就死给你看的条形码,十有八九就是在这个节骨眼上翻了车

静区那也是这符号身上掉下来的一块肉

一个明明编码挑不出半点毛病的条形码,最后要是死活扫不出来,那最大的冤大头绝对是静区(quiet zone)——也就是那些黑杠子两边留出的那片光秃秃的自留地。扫描器全指望着这片空地来摸清这码到底是从哪儿开张、又在哪儿打烊的;要是没这块地,阅读器根本分不清那第一根杠子和紧挨着它印在纸上的那些破玩意儿到底谁跟谁。这规矩可是白纸黑字写得死死的。绝大多数线性符号体系都死皮赖脸地要求两边至少得留出十倍于模块宽度的静区,而 UPC 和 EAN 这帮零售码里的地头蛇,更是变本加厉地要求左边得留出九个模块,右边得留出七个模块。就拿 1 磅的模块来算,这就是差不多十磅——差不多七分之一英寸的、必须由你拿命来担保的、寸草不生的一大片白地,就那么死死地守在那些杠子的两边

HotPDF 它就是个画杠子的苦力,别的它啥都不管。它绝对不会像个老妈子一样替你把静区给占好,这就意味着这口黑锅得由你自己背,而且一不留神就会忘得精光。这翻车的方式往往阴险得很:你可能只是顺手把个条形码紧贴着一个表格的框线放了进去,或者让页面排版把个公司 Logo 挤到了它跟前,结果就是,这个原本在一张大白纸上能通杀所有测试的条形码,一旦被塞进一份真枪实弹的文档里,当场就变成了个哑巴。你得像个守财奴一样把这块边距给抠出来。在喊出 DrawBarcode 之前,两边至少给我留出十个模块宽度的净空区,不管是哪个图形、哪根线、还是哪段文字,只要敢踏进这块禁地半步,那就是实打实的生产事故,别给我扯什么为了排版好看的鬼话

颜色、反差,还有底下那行给人看的人话(文字)

那俩专门管杠子和背景颜色的参数,不过是拿来给你凑品牌色卡用的个摆设,但它们绝对是毁掉一个本可以正常工作的条形码的最快捷径。扫描器那帮家伙可是全靠吃反差这口饭长大的,老掉牙的玩法是用红光扫,而且它们这辈子就只认浅色地盘上长出来的深色杠子。黑底白字是你唯一可以闭着眼睛往上拍、连测都不用测的组合。白底配个深蓝或者深绿勉强还能糊弄过去;但要是这俩颜色之间的亮度反差小得可怜,特别是你要是敢用红色的杠子——那红光扫描器看过去就是一片红彤彤的背景——那它绝对扫不出个屁来。要是有哪个设计师跑来跟你矫情说非要个彩色的条形码,你能给他的最老实的一句回答就是:这杠子必须得是黑的,你那五颜六色爱涂哪涂哪,别碰这标签就行了

DrawBarcode 这条路子还能顺手把挂在杠子底下那行给人看的人话(文字)给画出来,这可是留给收银员在扫描器罢工时,靠两根指头去戳键盘保命用的数据。这文字是最后的退路,不是啥花边装饰,所以你要是打算自己动手往旁边配说明,千万得离那个静区远点;一个被硬生生挤在旁边边距里的、标着这码是个啥符号体系的狗屁标签,绝对能瞬间毁掉扫描器赖以生存的那片白地。这篇示例里的这堆字段,还有那个用来画周围乱七八糟标签的 TextOut,全都在那篇 报告输出指南 里被扒得一干二净,要是你这码只是个被塞进复杂大页面里的一个小零件,那你绝对该去那儿翻翻老底

别嫌烦的最后验货规矩

矢量画出来的杠子绝对是个值得拿出来吹一把的大杀器。因为 DrawBarcode 是把这码当成 PDF 画图操作符给硬敲进去的,而不是随便贴一张被光栅化过的破图,所以不管你放大多大,这些杠子永远都是削铁如泥的利索,而且这文件本身压根不带啥分辨率的包袱;唯一能卡脖子的分辨率,就只剩下打印机自己的了。但这绝对不是你不去测试的借口,这只是意味着你这测试必须得落在纸上才算数。随便搞个样本出来,把它塞进你这批码以后能遇到的最烂、分辨率最低的那台破机器里打出来,然后拿着你的用户手里攥着的那种破扫码枪去扫,千万别拿你桌上那台几千块的高端货去自欺欺人。拿把尺子在打印出来的废纸上去量量那两边的静区,死死盯住那个模块宽度是不是活着挺过了从磅到墨点的这趟鬼门关,最后还得去对对账,看看它解出来的那串数,是不是跟你当初塞进去的一模一样,连那个校验位都得查。花五分钟拿把真枪扫两圈,上面抖出来的这些个翻车事故全都能给你揪出来,而且绝对是在那整整一卡车被贴错标签的货发出去之前就给你拦下

这儿拉出来示众的 DrawBarcodeDirectDrawBarcode 方法,全都是那套在 Delphi 和 C++Builder 里打拼的 HotPDF 组件 家的人