JsBarcode 转曲,将包含`<text>` 文本元素的svg 转为 path svg

工作业务中,使用到了 JsBarcode 用于生成条码图, 但是直接生成的条码图如果为 svg 格式的话,存在一些问题

  1. 字体丢失
  2. 无法旋转条码,我们需求它支持旋转
  3. 生成的svg 条码图中的文字部分直接是 <text> 元素,未转曲。

为了解决这个问题,我的解决思路是:

  1. 条码生成的时候插入base64格式的字体文件到 style 标签,追加到生成的svgNode
  2. 绘制一个 背景塞入
  3. 将生成的svg绘制到 canvas, 在canvas 上旋转
  4. 将canvas 通过位图转矢量图转换未转曲后的携带path 的 svg。

下面的演示仅包含关键逻辑代码, 并非完整代码

追加 字体 文件到svg,修复生成的svg条码字体丢失问题:

php 复制代码
......
// 创建svg节点,用于绘制 svg 条码
const svgNode = document.createElementNS(svgNS, "svg")
​
// 下面两行代码是因为我这边支持 png 和 svg 导出两种格式, 这边仅演示 options.export !== 'png' 的情况
const canvasNode = document.createElement("canvas")
const node = options.export === 'png' ? canvasNode : svgNode
​
// 使用 JSBarcode 绘制
JsBarcode(node, code, {
    textPosition: textPosition,
    format: options.symbology,
    lineColor: "#000",
    height: 40,
    margin: 20,
    font: "ocrb",
    //!需要本地指定 fontfaze https://github.com/lindell/JsBarcode/wiki/Options#font
    // 这里的 font 指定,在生成png格式(直接绘制到canvas)的时候,是正常的
    displayValue: true,
    background: '#FFFFFF'
});

修复字体:

arduino 复制代码
// @/assets/fonts/ocrb.base64
// base64 字体文件
export default "data:@file/x-font-ttf;base64,AAEAAAAOAIAAAwBgT1MvMoikfiEAAFAIAAAAVlBDTFR........
dart 复制代码
import ocrbfont from "@/assets/fonts/ocrb.base64"
​
fixFont(svgNode)
const fixFont = (svgNode: SVGSVGElement)=>{
    const style = document.createElementNS("http://www.w3.org/2000/svg", "style");
    style.textContent = `
    @font-face {
        font-family: "ocrb";
        src: url('${ocrbfont}');
      }    
    `
    svgNode.append(style)
}

修复后的 svgNode 输出转未 xmlString, 方便我们后续处理:

scss 复制代码
const xml = new XMLSerializer().serializeToString(svgNode)

支持旋转并转曲

JsBarcode 支持条码和文字的上下倒置,但是不支持旋转,

所谓旋转,其实就是把条码竖过来,所有符合规则的条码如下图所示:

所以我们只要判断什么收竖直排列条码就可以了, 至于文字如何排列可以在 jsBarcode 绘制阶段就能处理好,也就是 上面的 textPosition 字段。

如何完成旋转呢? 我们可以在canvas 上做到。

另一个问题是怎么将文字转为 path ? 这里经过一番调研,请教有经验的同事, 我知道了一个工具, 叫做 Potrace, 这是一个位图转矢量图的工具, 也就是你喂给它 png/jpeg之类的非矢量图, 它会自动帮你转为 svg path, 我们在web上使用, 这里有一个 wasm 对应的库 potrace-wasm (直呼大佬nb!!! 大佬救我狗命!)

一番bug之后,我写了一个工具函数,如下:

├── convertNormalSvg2Path
│   ├── convertCanvas2Svg.ts
│   └── index.ts
typescript 复制代码
// convertCanvas2Svg.ts
import { loadFromCanvas } from "potrace-wasm";
export default (canvas: HTMLCanvasElement, config?: { [x: string]: any }) => {
    return new Promise((resolve, reject) => {
        loadFromCanvas(canvas, config)
            .then((svg:string) => resolve(svg))
            .catch((err:Error) => reject(err));
    })
}
scss 复制代码
// index.ts
import convertCanvas2Svg from "./convertCanvas2Svg"
/**
 * svgxml: svgXml 字符串
 * scale: 放大倍数,值越大,转换结果越大,在容器固定时就会越清晰
 * isVertical: 横向/竖向
 */
​
export default (svgxml: string, scale: number = 2, isVertical: boolean):Promise<string> => {
    return new Promise((resolve, reject) => {
        const svgDoc = svgStr2Dom(svgxml)
        // 移除 svg 节点的 transform 属性
        svgDoc.removeAttribute('transform')
        const _svgxml = svgDom2Str(svgDoc)
​
        const url = 'data:image/svg+xml;charset=utf-8,' + encodeURIComponent(_svgxml);
        const img = new Image();
        img.src = url
        img.onload = async () => {
            if (svgDoc) {
                // 创建一个虚拟的 canvas 画布
                const canvas = document.createElement('canvas');
                // 按照指定规则绘制画布
                drawImg(canvas, svgDoc, img, isVertical, scale)
                try{
                    const newSvgXml = await convertCanvas2Svg(canvas) as string
                    resolve(newSvgXml)
                }catch(err){
                    reject(err)
                }
            }
        }
    })
​
}
​
​
// 设定画布信息
function setCanvas(canvas: HTMLCanvasElement, svgDoc: SVGElement, isVertical: boolean, scale: number) {
    const _width = svgDoc.getAttribute('width');
    const _height = svgDoc.getAttribute('height');
    if (_width && _height) {
        const width = parseInt(_width)
        const height = parseInt(_height)
        canvas.width = isVertical ? height * scale : width * scale
        canvas.height = isVertical ? width * scale : height * scale
    }
}
​
function drawImg(canvas: HTMLCanvasElement, svgDoc: SVGElement, img: HTMLImageElement, isVertical: boolean, scale: number) {
    setCanvas(canvas, svgDoc, isVertical, scale)
    const ctx = canvas.getContext('2d')
    if (!ctx) return
    ctx.scale(scale, scale);
​
    if (isVertical) {
        ctx.translate(img.height, 0);
        ctx.rotate((90 * Math.PI) / 180);
    }
​
    ctx.drawImage(img, 0, 0)
}
​
function svgDom2Str(svgElement: SVGElement) {
    const serializer = new XMLSerializer();
    // 使用 serializeToString 方法将 DOM 元素序列化为 XML 字符串
    return serializer.serializeToString(svgElement);
}
function svgStr2Dom(svgStr: string) {
    const parser = new DOMParser();
    return parser.parseFromString(svgStr, 'image/svg+xml').firstChild as SVGElement;
}

如下调用:

ini 复制代码
const isVertical = ['left', 'right'].includes(options.position)
const _xml = await convertNormalSvg2Path(xml, 10, isVertical)

特别注意第二个参数, 他是基于输入xml 的 <svg height="xx" width="xx"> 的宽高的放大倍数, 这个值越大,那么在canvas 上的绘制图也就越大, 喂给 potrace-wasm 之后,输出的精度就越高, 当然输出的 svg 尺寸也会更大。 这个值太小就会失真,就像这样

额外的问题: 转曲后的svg 背景丢失了

这个问题好解决,我们自己画一个背景就行了:

javascript 复制代码
+ import setBgc4PathSvg from "@/utils/setBgc4PathSvg"
.....
const isVertical = ['left', 'right'].includes(options.position)
const _xml = await convertNormalSvg2Path(xml, 10, isVertical)
+ const _xml_with_bgc = setBgc4PathSvg(_xml, '#FFFFFF')
arduino 复制代码
// @/utils/setBgc4PathSvg.ts
export default function setBackgroundColor(xml: string, color: string) {
    const parser = new DOMParser();
    const xmlDoc = parser.parseFromString(xml, 'image/svg+xml').lastChild as SVGElement;
    // 获取元素宽高
    const svgHeight = xmlDoc.getAttribute("height")!;
    const svgWidth = xmlDoc.getAttribute("width")!;
    console.log('svgHeight', svgHeight)
    // 绘制一个矩形背景
    const rect = document.createElementNS("http://www.w3.org/2000/svg", "rect");
    rect.setAttribute("x", '0');
    rect.setAttribute("y", '0');
    rect.setAttribute("xmlns", "http://www.w3.org/2000/svg");
    rect.setAttribute("width", Number(svgWidth).toFixed());
    rect.setAttribute("height", Number(svgHeight).toFixed());
    rect.setAttribute("fill", color);
​
    // 将矩形元素添加到 SVG 中
    // 获取svg节点下面的第一个元素
    const firstChild = xmlDoc.firstChild;
    // 将举行边框插入到第一个元素之前
    xmlDoc.insertBefore(rect, firstChild);
​
    // 返回 xmlstring
    const serializer = new XMLSerializer();
    return serializer.serializeToString(xmlDoc);
}

完美!

相关推荐
阿伟来咯~19 分钟前
记录学习react的一些内容
javascript·学习·react.js
吕彬-前端24 分钟前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js
学前端的小朱27 分钟前
Redux的简介及其在React中的应用
前端·javascript·react.js·redux·store
guai_guai_guai36 分钟前
uniapp
前端·javascript·vue.js·uni-app
也无晴也无风雨37 分钟前
在JS中, 0 == [0] 吗
开发语言·javascript
bysking2 小时前
【前端-组件】定义行分组的表格表单实现-bysking
前端·react.js
王哲晓2 小时前
第三十章 章节练习商品列表组件封装
前端·javascript·vue.js
fg_4112 小时前
无网络安装ionic和运行
前端·npm
理想不理想v2 小时前
‌Vue 3相比Vue 2的主要改进‌?
前端·javascript·vue.js·面试
酷酷的阿云2 小时前
不用ECharts!从0到1徒手撸一个Vue3柱状图
前端·javascript·vue.js