.net core 连接打印机 使用zpl指令打印机,并且位图本地渲染zpl指令

Java后端ZPL标签打印:中文二维码生成与常见问题排查指南 标签打印是企业信息化中的常见需求,尤其在仓储、医疗、零售等领域,需要高效生成包含二维码和中文信息的标签。ZPL是Zebra打印机的原生指令语言,通过简单的文本指令即可精确控制打印位置、字体和条码格式,是Java后端与打印设备交互的通用协议。理解ZPL的坐标体系、字符编码和数据传输机制,是解决中文乱码、二维码无法识别等问题的关键。借助Java生态中的ZXing和Socket通信,可以实现从数据生成到打印机输出的完整链路。本文从ZPL基础指令出发,详细讲解如何通过^GF指令渲染中文位图、使用^BQ生成二维码,并 阅读详情

简介:本章主要使用BinaryKits组件将zpl 指令远程连接打印机打印机内容,且通过解析zpl指令在本地软件预览,实时编辑,兼容Windows、Linux。支持中文、英文、特殊字符等,不依赖系统字体。开源、免费、国产化

1.引入核心组件

Install-Package BinaryKits.Zpl.Label 版本3.3.1

Install-Package BinaryKits.Zpl.Viewer 版本1.2.0

我测试1.2.0版本兼容,升级到3.0版本或其他版本,对ZplElementDrawer在进行绘制Draw时,DrawerOptions调用字体文件非public,无法指定字体文件。

各位道友不做文字处理,可以配置服务器字体文件,但是要担心SkiaSharp默认指向。

Install-Package SkiaSharp 版本2.88.8

Install-Package SkiaSharp.HarfBuzz 版本2.88.8

Install-Package SkiaSharp.NativeAssets.Linux.NoDependencies 版本2.88.8

该三个组件必须保持版本一致,不然会报错

错误:SkiaSharp Value cannot be null. (Parameter 'asset')或者依赖项指定错误、资源文件找不到等等

2.项目核心配置

<PublishSingleFile>false</PublishSingleFile>

禁用单文件发布,强制使用项目框架依赖

3.上传页面

Razor代码

@Html.AppendCssFile("/Areas/ResourceFile/UploadifyForm.css")
<div class="lr-form-layout lr-form-layout-top-bottom">
    <div class="lr-form-layout-header">
        <div id="lr_add_file_btn"><i class="fa fa-cloud-upload"></i>添加文件</div>
    </div>
    <div class="lr-form-layout-body">
        <div class="lr-form-file-queue-wrap" id="lr_form_file_queue">
            <div class="lr-form-file-queue" id="lr_form_file_queue_list">
                
            </div>
            <div class="lr-form-file-queue-bg">
                <h1>试试将电脑里的文件拖拽到此上传</h1>
            </div>
        </div>
    </div>
</div>
<script src="~/Content/webuploader/webuploader.nolog.min.js"></script>
@Html.AppendJsFile("/Areas/Views/UploadifyForm.js")

css代码

.lr-form-layout-body {
    padding: 0 5px 5px 5px;
}

.lr-form-file-queue-wrap {
    position: relative;
    width: 100%;
    height: 100%;
    border: 1px solid #ddd;
    border-radius: 4px;
}

.lr-form-file-queue {
    position: relative;
}

.lr-form-file-queue-item {
    position: relative;
    width: 100%;
    height: 50px;
    border-bottom: 1px solid #ddd;
    padding-left: 60px;
}

.lr-file-image {
    position: absolute;
    top: 5px;
    left: 5px;
}

    .lr-file-image img {
        width: 40px;
        height: 40px;
    }

.lr-file-name {
    line-height: 50px;
}

.lr-msg {
    position: absolute;
    top: 0;
    right: 5px;
    font-size: 25px;
    line-height: 50px;
}

    .lr-msg .fa-check-circle {
        color: #5cb85c;
    }

    .lr-msg .fa-exclamation-circle {
        color: #b94a48;
    }

.lr-tool-bar {
    position: absolute;
    top: 0;
    right: 35px;
    font-size: 25px;
    line-height: 50px;
    color: #666;
    cursor: pointer;
}

    .lr-tool-bar > i {
        margin-left: 8px;
    }

    .lr-tool-bar .fa-minus-circle {
        color: #b94a48;
    }

    .lr-tool-bar .fa-cloud-download {
        color: #3498db;
    }

.lr-uploader-progress {
    position: absolute;
    bottom: 2px;
    left: 60px;
    height: 4px;
    width: 500px;
    width: calc(100% - 90px);
}

.lr-uploader-progress-bar {
    position: relative;
    height: 100%;
    background-color: #039cfd;
    border-radius: 4px;
}


.lr-form-file-queue-bg {
    position: absolute;
    top: 50%;
    left: 50%;
    width: 300px;
    height: 30px;
    line-height: 30px;
    margin: -15px 0 0 -150px;
}

    .lr-form-file-queue-bg h1 {
        color: #666;
        font-size: 20px;
        font-family: Microsoft Yahei;
        padding-bottom: 2px;
        margin: 0;
    }

/*对百度插件的样式重写*/
.webuploader-pick {
    padding: 8px 15px;
    background-color: #039cfd;
}

.webuploader-pick-hover {
    background-color: #027dcb;
}

js代码

            page.uploader = WebUploader.create({
                auto: true,
                swf: top.$.rootUrl + '/Content/webuploader/Uploader.swf',
                // 文件接收服务端。
                server: top.$.rootUrl + '/UploadifyFile',
                // 选择文件的按钮。可选。
                // 内部根据当前运行是创建,可能是input元素,也可能是flash.
                pick: '#lr_add_file_btn',
                dnd: '#lr_form_file_queue',
                paste: 'document.body',
                accept: {
                    extensions: "zpl"
                },
                multiple: true,
                // 上传参数
                formData: {
                    __RequestVerificationToken: $.lrToken
                }
            });
            page.uploader.on('fileQueued', page.fileQueued);
            page.uploader.on('uploadStart', page.uploadStart);
            page.uploader.on('uploadBeforeSend', page.uploadBeforeSend);
            page.uploader.on('uploadProgress', page.uploadProgress);
            page.uploader.on('uploadSuccess', page.uploadSuccess);
            page.uploader.on('uploadError', page.uploadError);
            page.uploader.on('uploadComplete', page.uploadComplete);
            page.uploader.on('error', page.error);


        }

控制代码

        /// <summary>
        /// 上传文件
        /// </summary>
        /// <returns></returns>
        [HttpPost]
        public async Task<ActionResult> UploadifyFile()
        {
            try
            {
                //没有文件上传,直接返回
                var Filedata = Request.Form.Files["file"];
                if (Filedata == null || string.IsNullOrEmpty(Filedata.FileName) || Filedata.Length == 0)
                {
                    return NotFound();
                }
                //获取文件完整文件名(包含绝对路径)
                //文件存放路径格式:/TagTemplate/{guid}.{后缀名}
                string fileGuid = Guid.NewGuid().ToString();
                long filesize = Filedata.Length;

                // HttpContext.Request 里拿
                var req = HttpContext.Request;
                // 1. 协议:http / https
                string scheme = req.Scheme;
                // 2. Host(域名+端口) 例:172.16.42.206:8201
                string host = req.Host.ToString();
                // 3. 基础地址(协议+host) 
                string baseUrl = $"{req.Scheme}://{req.Host}";
                string httpUrl = $"{baseUrl}/TagTemplate/{fileGuid}.zpl";

                string fullFileName = Path.Combine(_hostEnvironment.WebRootPath, "TagTemplate", string.Format("{0}.zpl", fileGuid));

                //创建文件夹
                string path = Path.GetDirectoryName(fullFileName);
                if (!Directory.Exists(path))
                {
                    Directory.CreateDirectory(path);
                }
                if (!System.IO.File.Exists(fullFileName))
                {
                    //保存文件
                    using (var stream = new FileStream(fullFileName, FileMode.Create))
                    {
                        await Filedata.CopyToAsync(stream);
                    }
                    //文件信息写入数据库
                    FileInfoEntity fileInfoEntity = new FileInfoEntity();
                    fileInfoEntity.Create();
                    fileInfoEntity.F_FileId = fileGuid;
                    if (!string.IsNullOrEmpty(folderId))
                    {
                        fileInfoEntity.F_FolderId = folderId;
                    }
                    else
                    {
                        fileInfoEntity.F_FolderId = "0";
                    }
                    fileInfoEntity.F_FileName = Filedata.FileName;
                    fileInfoEntity.F_EnabledMark = 0;
                    fileInfoEntity.F_FilePath = httpUrl;
                    fileInfoEntity.F_FileSize = filesize.ToString();
                    fileInfoEntity.F_FileExtensions = ".zpl";
                    fileInfoEntity.F_FileType = "zpl";
                    fileInfoBLL.SaveForm("", fileInfoEntity);
                    return Success("上传成功。");
                }
                else
                {
                    return Fail("文件已经存在。");
                }
            }
            catch (Exception ex)
            {
                return Fail(ex.Message);
            }
        }

4.预览页面

Razor代码

@Html.AppendCssFile("/Areas/Views/PreviewZpl.css")
<div class="container">
    <!-- 顶部工具栏 -->
    <div class="toolbar">
        <span class="file-name" id="fileName"></span>
        <a id="lr_refresh" class="btn btn-default"><i class="fa fa-refresh"></i>&nbsp;刷新</a>
        <a id="lr_print" class="btn btn-default"><i class="fa fa-print"></i>&nbsp;打印</a>
        <a id="lr_save" class="btn btn-default"><i class="fa fa-save">&nbsp;保存</i></a>
    </div>

    <!-- 主体:左编辑,右预览 -->
    <div class="main-content">
        <!-- 左侧 ZPL 编辑器 -->
        <div class="panel">
            <div class="panel-header">
                <i class="layui-icon layui-icon-edit"></i> ZPL 源代码
            </div>
            <textarea id="zplEditor" placeholder="在此输入或编辑ZPL指令..."></textarea>
        </div>

        <!-- 右侧预览 -->
        <div class="panel">
            <div class="panel-header">
                <i class="layui-icon layui-icon-picture"></i> 标签预览
            </div>
            <div class="preview-wrapper" id="previewWrapper">
                <img id="previewImg" />
            </div>
        </div>
    </div>
</div>
@Html.AppendJsFile("/Areas/Views/PreviewZpl.js")

css代码

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body, html {
    width: 100%;
    height: 100%;
    font-family: "Microsoft YaHei", sans-serif;
    overflow: hidden;
}

.container {
    display: flex;
    flex-direction: column;
    width: 100%;
    height: 100%;
}

/* 顶部工具栏 */
.toolbar {
    height: 50px;
    background: #f8f8f8;
    border-bottom: 1px solid #e6e6e6;
    display: flex;
    align-items: center;
    padding: 0 20px;
    gap: 10px;
}

    .toolbar .file-name {
        flex: 1;
        font-size: 14px;
        color: #333;
        font-weight: 500;
    }

    .toolbar .btn {
        padding: 6px 18px;
        border: none;
        border-radius: 4px;
        cursor: pointer;
        font-size: 13px;
        transition: all 0.2s;
    }

.btn-save {
    background: #1E9FFF;
    color: #fff;
}

    .btn-save:hover {
        background: #0d8de6;
    }

.btn-close {
    background: #ff5722;
    color: #fff;
}

    .btn-close:hover {
        background: #e64a19;
    }

.btn-refresh {
    background: #67c23a;
    color: #fff;
}

    .btn-refresh:hover {
        background: #5aaf2f;
    }

/* 主体区域 */
.main-content {
    flex: 1;
    display: flex;
    overflow: hidden;
}

.panel {
    width: 50%;
    height: 100%;
    display: flex;
    flex-direction: column;
    border-right: 1px solid #e6e6e6;
}

    .panel:last-child {
        border-right: none;
    }

.panel-header {
    height: 40px;
    background: #f5f5f5;
    display: flex;
    align-items: center;
    padding: 0 15px;
    font-size: 13px;
    color: #666;
    border-bottom: 1px solid #e6e6e6;
    font-weight: 500;
}

/* 左侧编辑器 */
#zplEditor {
    flex: 1;
    width: 100%;
    padding: 12px;
    border: none;
    outline: none;
    resize: none;
    font-family: "Consolas", "Monaco", "Courier New", monospace;
    font-size: 13px;
    line-height: 1.6;
    background: #1e1e1e;
    color: #d4d4d4;
    tab-size: 4;
}

    #zplEditor::-webkit-scrollbar {
        width: 10px;
        height: 10px;
    }

    #zplEditor::-webkit-scrollbar-track {
        background: #2d2d2d;
    }

    #zplEditor::-webkit-scrollbar-thumb {
        background: #555;
        border-radius: 5px;
    }

        #zplEditor::-webkit-scrollbar-thumb:hover {
            background: #666;
        }

/* 右侧预览区 */
.preview-wrapper {
    flex: 1;
    background: #e0e0e0;
    overflow: auto;
    display: flex;
    align-items: flex-start;
    justify-content: center;
    padding: 20px;
}

.preview-canvas {
    background: #fff;
    box-shadow: 0 2px 10px rgba(0,0,0,0.15);
}

.preview-empty {
    color: #999;
    font-size: 14px;
    text-align: center;
    margin-top: 50px;
}

js代码

            // 刷新
            $('#lr_refresh').on('click', function () {
                var imgUrl = top.$.rootUrl + '/GetZplFileImage?zpl=' + $('#zplEditor').val();
                $('#previewImg').attr("src", imgUrl);
            });
            // 打印
            $('#lr_print').on('click', function () {
                var postData = {};
                postData.zpl = $('#zplEditor').val();
                // 调用力软保存表单方法
                $.lrSaveForm(top.$.rootUrl + '/PrintZpl', postData, function (res) {

                }, function (errMsg) {
                    //保存失败回调:不关闭,只提示错误
                    learun.alert.error(errMsg);
                });
            });
            // 保存
            $('#lr_save').on('click', function () {
                var postData = {};
                postData.keyValue = keyValue;
                postData.zpl = $('#zplEditor').val();

                // 调用力软保存表单方法
                $.lrSaveForm(top.$.rootUrl + '//SaveZpl', postData, function (res) {
                    // 刷新父页面表格
                    top.$("#gridtable").trigger("reloadGrid");
                    // 关闭当前弹窗
                    learun.layerClose(window.name);
                }, function (errMsg) {
                    //保存失败回调:不关闭,只提示错误
                    learun.alert.error(errMsg);
                });
            });
        InitData: function () {
            if (!!keyValue) {
                $.lrSetForm(top.$.rootUrl + '//GetZplFileContent?keyValue=' + keyValue, function (data) {
                    $('#zplEditor').val(data.content);
                    var imgUrl = top.$.rootUrl + '/GetZplFileImage?zpl=' + data.content;
                    $('#previewImg').attr("src", imgUrl);
                });
            }
        },

控制器代码

        /// <summary>
        /// 获取zpl指令内容
        /// </summary>
        /// <param name="keyValue">keyValue主键</param>
        /// <returns></returns>
        [HttpGet]
        public ActionResult GetZplFileContent(string keyValue)
        {
            var data = fileInfoBLL.GetEntity(keyValue);
            string filename = WebUtility.UrlDecode(data.F_FileName);//客户端保存的文件名  
            string filepath = Path.Combine(_hostEnvironment.WebRootPath, "TagTemplate", string.Format("{0}.zpl", data.F_FileId));

            // 1. 读取文件内容
            string content = System.IO.File.ReadAllText(filepath);
            var jsonData = new
            {
                content = content
            };
            return Success(jsonData);
        }

        /// <summary>
        /// ZPL预览接口
        /// </summary>
        /// <param name="zpl">ZPL字符串</param>
        /// <returns></returns>
        [HttpGet]
        public ActionResult GetZplFileImage(string zpl)
        {
            try
            {
                if (string.IsNullOrWhiteSpace(zpl))
                {
                    return Fail("ZPL指令不能为空");
                }
                var imgBytes = ZplPrintHelper.RenderZplToPng(zpl);
                if (imgBytes == null || imgBytes.Length == 0)
                {
                    return Fail("渲染失败,无标签数据");
                }
                return File(imgBytes, "image/png");
            }
            catch (Exception ex)
            {
                return Fail(ex.Message);
            }
        }

        [HttpPost]
        public ActionResult SaveZpl(string keyValue, string zpl)
        {
            var data = fileInfoBLL.GetEntity(keyValue);
            string filename = WebUtility.UrlDecode(data.F_FileName);//客户端保存的文件名  
            string filepath = Path.Combine(_hostEnvironment.WebRootPath, "TagTemplate", string.Format("{0}.txt", data.F_FileId));
            data.F_ModifyDate = DateTime.Now;
            fileInfoBLL.SaveForm(keyValue, data);
            System.IO.File.WriteAllText(filepath, zpl);
            return Success("保存成功");
        }

        [HttpPost]
        public async Task<ActionResult> PrintZpl(string zpl)
        {
            string Message = string.Empty;
            if (!await ZplPrintHelper.IsPrinterOnlineAsync("192.168.123.100"))
            {
                Message = $"打印机不在线,请检查网络或电源";
                return Fail(Message);
            }
            bool success = await ZplPrintHelper.SendZplAsync(zpl, "192.168.123.100");
            Message = $"打印成功";
            return Success(Message);
        }

5.打印核心处理类

各位道友自己封装吧,我都写在一个类里面

    public class ZplPrintHelper
    {
        /// <summary>
        /// 离线ZPL转PNG图片,内网离线,无外网,纯条码/二维码/英文数字
        /// </summary>
        /// <param name="zpl">ZPL指令文本</param>
        /// <returns>png字节数组</returns>
        public static byte[] RenderZplToPng(string zpl, IWebHostEnvironment _hostEnvironment)
        {
            // 正则读取 ^PW(宽度) ^LL(高度)
            int pw = 400;
            int ll = 300;
            var matchPW = Regex.Match(zpl, @"\^PW(\d+)");
            var matchLL = Regex.Match(zpl, @"\^LL(\d+)");
            if (matchPW.Success) pw = int.Parse(matchPW.Groups[1].Value);
            if (matchLL.Success) ll = int.Parse(matchLL.Groups[1].Value);

            // 打印机存储,必须传入,存放图片、下载资源
            IPrinterStorage printerStorage = new PrinterStorage();
            var analyzer = new ZplAnalyzer(printerStorage);
            var analyzeResult = analyzer.Analyze(zpl);

            // 新版构造只需要 printerStorage
            DrawerOptionsHelper.HostEnvironment = _hostEnvironment;
            DrawerOptions opt = new DrawerOptions
            {
                FontLoader = DrawerOptionsHelper.CustomTtfFontLoader,
                OpaqueBackground = true,
                Antialias = true
            };
            var drawer = new ZplElementDrawer(printerStorage, opt);

            foreach (var labelInfo in analyzeResult.LabelInfos)
            {
                byte[] pngBytes = drawer.Draw(labelInfo.ZplElements);
                if (pngBytes == null || pngBytes.Length == 0)
                {
                    LogHelper.Error("RenderZplToPng:drawer.Draw失败,渲染资源缺失", "ZPL");
                    return null;
                }
                else
                {
                    // 裁剪到 ^PW x ^LL 画布
                    pngBytes = CropToLabelSize(pngBytes, pw, ll);
                    return pngBytes;
                }
            }
            return null;
        }

        /// <summary>
        /// 把渲染出来的大图,裁剪居中放到指定宽高的白底画布
        /// </summary>
        private static byte[] CropToLabelSize(byte[] sourcePng, int targetW, int targetH)
        {
            using var srcBmp = SKBitmap.Decode(sourcePng);
            if (srcBmp == null)
            {
                LogHelper.Error("CropToLabelSize:SKBitmap.Decode解码失败,无效PNG", "ZPL");
                return null;
            }

            // 目标画布:严格PW × LL,透明底
            using var destBmp = new SKBitmap(targetW, targetH, SKColorType.Bgra8888, SKAlphaType.Premul);
            using var canvas = new SKCanvas(destBmp);
            canvas.Clear(SKColors.Transparent);

            // =========重点:offsetX=0,offsetY=0!左上角对齐,不居中=========
            // ZPL ^FO(x,y) 就是相对于标签左上角的坐标,居中一定会错位!
            canvas.DrawBitmap(srcBmp, 0, 0);

            using var img = SKImage.FromBitmap(destBmp);
            using var data = img.Encode(SKEncodedImageFormat.Png, 100);
            return data.ToArray();
        }

        /// <summary>
        /// 检测打印机是否在线(能否建立 TCP 连接)
        /// </summary>
        /// <param name="_printerIp">打印机IP</param>
        /// <param name="_port">打印机端口 默认9100</param>
        /// <param name="_timeoutMs">超时</param>
        /// <returns></returns>
        public static async Task<bool> IsPrinterOnlineAsync(string _printerIp,int _port=9100,int _timeoutMs=5000)
        {
            try
            {
                using (var client = new TcpClient())
                {
                    if (!IPAddress.TryParse(_printerIp, out var ipAddress))
                        return false;

                    // 使用 Task.Run 包装 BeginConnect,避免 FromAsync 的陷阱
                    var connectTask = Task.Run(() =>
                    {
                        IAsyncResult ar = client.BeginConnect(ipAddress, _port, null, null);
                        try
                        {
                            // 等待完成
                            ar.AsyncWaitHandle.WaitOne(_timeoutMs);
                            if (ar.IsCompleted)
                            {
                                client.EndConnect(ar); // 安全调用
                                return true;
                            }
                            else
                            {
                                return false; // 超时
                            }
                        }
                        catch (SocketException)
                        {
                            return false;
                        }
                        finally
                        {
                            ar.AsyncWaitHandle.Close();
                        }
                    });

                    var success = await connectTask;
                    return success;
                }
            }
            catch (Exception)
            {
                return false;
            }
        }

        /// <summary>
        /// 异步发送 ZPL 指令到打印机
        /// </summary>
        /// <param name="zplCommand">完整的 ZPL 指令字符串</param>
        /// <param name="_printerIp">打印机IP</param>
        /// <param name="_port">打印机端口 默认9100</param>
        /// <param name="_timeoutMs">超时</param>
        /// <returns>成功返回 true</returns>
        public static async Task<bool> SendZplAsync(string zplCommand, string _printerIp, int _port = 9100, int _timeoutMs = 5000)
        {
            if (string.IsNullOrWhiteSpace(zplCommand))
                throw new ArgumentException("ZPL 指令不能为空", nameof(zplCommand));

            using (var client = new TcpClient())
            {
                if (!IPAddress.TryParse(_printerIp, out var ipAddress))
                    throw new ArgumentException($"无效的 IP 地址: {_printerIp}", nameof(_printerIp));

                // 使用 Task.Run 在后台线程执行同步阻塞的 WaitOne(避免死锁)
                await Task.Run(() =>
                {
                    IAsyncResult ar = null;
                    try
                    {
                        // 启动异步连接
                        ar = client.BeginConnect(ipAddress, _port, null, null);

                        // 等待连接完成或超时
                        bool completed = ar.AsyncWaitHandle.WaitOne(_timeoutMs);
                        if (!completed)
                        {
                            throw new TimeoutException($"连接打印机 {_printerIp}:{_port} 超时({_timeoutMs}ms)");
                        }

                        // 安全调用 EndConnect(此时 ar != null 且已完成)
                        client.EndConnect(ar);
                    }
                    catch (SocketException ex)
                    {
                        throw new Exception($"无法连接到打印机 {_printerIp}:{_port} - {ex.Message}", ex);
                    }
                    finally
                    {
                        ar?.AsyncWaitHandle?.Close(); // 释放等待句柄
                    }
                });

                // 连接成功,发送数据
                using (var stream = client.GetStream())
                {
                    byte[] data = Encoding.UTF8.GetBytes(zplCommand);
                    await stream.WriteAsync(data, 0, data.Length);
                    await stream.FlushAsync();
                }
            }
            return true;
        }
    }

6.文字处理类

该类主要处理字体文件,不依赖系统字体文件。字体文件各位道友自己找镜像文件吧。

    public class DrawerOptionsHelper
    {
        /// <summary>
        /// web下发路径
        /// </summary>
        public static IWebHostEnvironment HostEnvironment = null;
        // 字体缓存,全局复用SKTypeface,防止内存泄漏
        private static readonly ConcurrentDictionary<string, SKTypeface> _fontCache = new();

        /// <summary>
        /// 自定义字体加载:优先读取本地ttf,失败再回退系统字体
        /// fontName:ZPL ^A0 / ^A1 传进来的标识
        /// </summary>
        public static Func<string, SKTypeface> CustomTtfFontLoader = fontName =>
        {
            try
            {
                // 命中缓存直接返回
                if (_fontCache.TryGetValue(fontName, out var cacheTf))
                {
                    return cacheTf;
                }

                SKTypeface typeface = null;
                var fontMgr = SKFontManager.Default;
                var families = fontMgr.FontFamilies.ToList();

                // ========== 【重点】fontName=0 对应 ZPL ^A0,这里加载本地TTF ==========
                if (fontName == "0" || fontName == "A" || !string.IsNullOrEmpty(fontName))
                {
                    // 字体文件路径,可配置,建议绝对路径
                    string ttfPath = Path.Combine(HostEnvironment.WebRootPath, "ZplFonts", "LZHONGHEI.ttf");
                    if (File.Exists(ttfPath))
                    {
                        using var stream = File.OpenRead(ttfPath);
                        typeface = SKTypeface.FromStream(stream);
                    }

                    // TTF加载失败,回退原来系统字体逻辑
                    if (typeface == null)
                    {
                        if (families.Contains("Helvetica"))
                        {
                            typeface = SKTypeface.FromFamilyName("Helvetica", SKFontStyleWeight.Bold, SKFontStyleWidth.SemiCondensed, SKFontStyleSlant.Upright);
                        }
                        else if (families.Contains("Roboto Condensed"))
                        {
                            typeface = SKTypeface.FromFamilyName("Roboto Condensed", SKFontStyleWeight.Bold, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
                        }
                        else if (families.Contains("Swis721 BT"))
                        {
                            typeface = SKTypeface.FromFamilyName("Swis721 BT");
                        }
                        else if (families.Contains("Arial"))
                        {
                            typeface = SKTypeface.FromFamilyName("Arial", SKFontStyleWeight.Bold, SKFontStyleWidth.Condensed, SKFontStyleSlant.Upright);
                        }
                        else
                        {
                            typeface = SKTypeface.FromFamilyName("Helvetica", SKFontStyleWeight.Bold, SKFontStyleWidth.SemiCondensed, SKFontStyleSlant.Upright);
                        }
                    }
                }
                else
                {
                    // 其它字体标识(^A1、^A2等)沿用原有英文等宽逻辑
                    if (families.Contains("DejaVu Sans Mono"))
                    {
                        typeface = SKTypeface.FromFamilyName("DejaVu Sans Mono", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
                    }
                    else if (families.Contains("Courier New"))
                    {
                        typeface = SKTypeface.FromFamilyName("Courier New", SKFontStyleWeight.Medium, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
                    }
                    else
                    {
                        typeface = families.Contains("Courier")
                            ? SKTypeface.FromFamilyName("Courier", SKFontStyleWeight.Medium, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright)
                            : SKTypeface.FromFamilyName("DejaVu Sans Mono", SKFontStyleWeight.Normal, SKFontStyleWidth.Normal, SKFontStyleSlant.Upright);
                    }
                }

                // 兜底,防止null
                typeface ??= SKTypeface.Default;

                _fontCache.TryAdd(fontName, typeface);
                return typeface;
            }
            catch (Exception ex)
            {
                LogHelper.Error($"DrawerOptionsHelper:CustomTtfFontLoader失败,{ex.Message}", "ZPL");
                return null;
            }
        };
    }

结果展示

编辑模板完成,点击刷新,道友们也可以自行zpl文本改变实时刷新

SilentPrint静默打印中间件:解决网页打印弹窗打断问题 静默打印是工业控制、医疗终端、政务自助机等场景中保障UI连续性的关键技术,其本质是在浏览器安全策略限制下,绕过window.print()的弹窗与焦点劫持缺陷,通过服务端渲染+本地代理实现无感打印。核心原理在于将打印任务从UI线程剥离,交由可信上下文(如本地Agent)调用系统API执行,从而规避用户激活事件限制和跨域安全拦截。该方案具备离线支持、ZPL指令直打、动态内容注入、任务队列调度等工程价值,广泛应用于电子票据、工单标签、诊断报告等需高可靠输出的B端系统。 阅读详情

相关推荐

stb-translator-data-repository.zip - Web 版翻译者资料库项目源码

本项目是由 VB6/Qt 桌面版《翻译者资料库》移植而来的 Web 版本,采用 Spring Boot 3.3 + Thymeleaf + Bootstrap 5.3 + MySQL 8.0 技术栈,面向翻译与写作场景提供一站式资料管理底座。系统涵盖资料管家、分类词库、句库、文本处理器、中英对照制作、通用查找、数据备份恢复及旧库迁移等核心模块,完整继承桌面版的编号连续、引号规范化、查重、RTF 附件同步等关键业务规则。前端采用服务端渲染配合本地静态资源,支持离线使用;通过 run.bat 一键完成环境检查、建库、编译与启动,访问 http://localhost:8080 即可使用。

Java二维码标签生成与打印全解析:从ZXing到DPI匹配

二维码生成和标签打印是仓储管理、资产标识、产品溯源等场景中的核心基础技术。在实际工程中,开发者常借助ZXing等库生成二维码矩阵,再通过Graphics2D渲染为可打印的标签图片。但真正影响打印质量的关键,往往在于对静区宽度、纠错级别、物理尺寸与打印机DPI之间精确换算的掌控。图片像素与打印机分辨率若不对齐,或驱动设置了额外缩放,就极易导致二维码模糊、无法扫描。同时,批量打印时的性能优化、跨平台字体渲染以及ESC/POS、ZPL等指令集的选择,也决定了方案能否稳定落地。本文围绕Java生态,从二维码参数设置

weixin_30399155的博客 396

利用LQR反馈控制器在不同风况下模拟风力涡轮转速控制的MATLAB.zip

1.版本:matlab2014a/2019b/2024b 2.附赠案例数据可直接运行。 3.代码特点:参数化编程、参数可方便更改、代码编程思路清晰、注释明细。 4.适用对象:计算机,电子信息工程、数学等专业的大学生课程设计、期末大作业和毕业设计。

二维装箱

//节点 public class Node { public int X { get; set; } public int Y { get; set; } public int W { get; set; } public int H { get; set; } public bool Is...

8370

基于jdk21、jdk17、jdk8 + SpringCloud + SpringBoot 开发的微服务中后台快速开发平台,专注于多租户、开放平台解决方案,亦可作为普通项目的基础开发框架使用

基于jdk21、jdk17、jdk8 + SpringCloud + SpringBoot 开发的微服务中后台快速开发平台,专注于多租户(SaaS架构)、开放平台解决方案,亦可作为普通项目(非SaaS架构)的基础开发框架使用,目前已实现数据库隔离、字段隔离等租户隔离方案。

【无人机控制】基于MATLAB的侧风扰动抑制与风力模型构建:位置误差评估及风场感知路线规划系统设计 MATLAB 无人机侧风扰动抑制|风力模型与位置误差评估

内容概要:本文档围绕无人机在侧风扰动下的飞行稳定性问题,构建了一个基于MATLAB的完整仿真框架,涵盖风力建模、无人机运动仿真、侧风抑制控制、风场感知路线规划及位置误差评估。项目通过五种独立机制(平均风速、风向投影、阵风、湍流、定位噪声)生成具有物理意义的模拟数据,构建二维无人机运动模型,并设计包含比例-积分-前馈的复合控制器以抑制侧风引起的横向漂移。路线规划采用融合距离、风场与转弯代价的A*算法,实现抗扰路径选择。系统提供多层次误差评价体系,并支持结果可视化与模型部署,适用于低空飞行任务的抗风性能分析与优化。; 适合人群:具备MATLAB编程基础,从事无人机控制、路径规划或自动化系统开发的研发人员,尤其适合工作1-3年、希望深入理解环境扰动建模与控制系统协同设计的工程师。; 使用场景及目标:①研究侧风环境下无人机航迹跟踪的误差来源与抑制方法;②对比不同控制策略(如无补偿、积分补偿、前馈补偿)在侧风扰动中的性能差异;③实现融合风场代价的智能路径规划,提升任务安全性与能效;④基于模拟数据训练误差预测模型,支持数据驱动的飞行优化。; 阅读建议:此资源强调可复现性与工程实用性,建议读者结合代码逐模块运行,重点关注风场建模机制、控制律设计、路线代价函数构建及多指标评估方法。应通过调整参数(如风场权重、控制增益)观察系统响应变化,并利用提供的GUI界面进行交互式调试,以深化对各模块协同机制的理解。

C# HalconDotNet 工业视觉处理

1.引入Halcon组件 2.项目配置文件 <runtime> <assemblyBinding xmlns="urn:schemas-microsoft-com:asm.v1"> <dependentAssembly> <assemblyIdentity name="halcondotnet" publicKeyToken="4973BED59DDBF2B8"/> <bindingRed

4974

高校技术转移办公室人员如何提升技术成果的推介效果和市场转化率?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

C# Socket 读取可变长度数据

byte[] allContent = null; int AllLength = 0; int count = 0; int length= 1024; byte[] readBuff = new byte[length]; List<byte[]> readBuffList = new List<byte[]>(); while (true) { int r = this._clientSocket.Receive(readBuff, 0, length, Socket.

1494

两相交错并联BuckBoost变换器仿真模型(Simulink仿真实现)

内容概要:本文档系统整合了电力电子与新能源领域的Simulink仿真资源,聚焦于两相交错并联Buck-Boost变换器的双闭环PWM控制建模与仿真,同时涵盖多种典型拓扑结构,包括交错并联Boost、PFC功率因数校正、LLC谐振变换器、双向CLLC、DAB变换器、VIENNA整流器等。资源体系完整,包含仿真模型、设计报告、计算书、PPT说明文档及Matlab/Python代码,覆盖从基础建模到高级控制策略(如软开关技术、谐波抑制、状态估计、双闭环控制)的全流程。此外,还拓展至状态估计(卡尔曼滤波、粒子滤波)、智能优化算法、机器学习、信号处理、路径规划等多个交叉学科方向,构建了一个面向科研与工程应用的综合性技术平台。; 适合人群:电气工程、自动化、电力电子、新能源科学与工程等专业的硕士博士研究生、高校科研人员,以及从事新能源发电、智能电网、电动汽车、电力传动等领域技术研发的工程师。; 使用场景及目标:①开展电力电子变换器(如Buck-Boost、LLC、DAB、PFC等)的建模、控制与仿真研究;②支撑课程设计、学位论文、科研项目中的系统仿真与算法验证;③学习并复现高水平期刊(如EI)提出的先进控制策略,如基于KKT条件的最优控制、扩展移相调制、恒压恒流双环竞争控制等;④结合智能算法(如粒子群、秃鹰算法)进行微电网调度、储能配置、路径规划等多目标优化问题的研究与快速原型开发。; 阅读建议:建议依据提供的资源目录循序渐进地查阅资料,优先选取与自身研究方向高度契合的内容进行深入学习;应结合仿真模型与配套文档动手实践,调试参数并分析结果,充分利用百度网盘与公众号渠道获取完整资源,提升科研工作的系统性、规范性与创新性。

【基于Zernike矩的良性和恶性肿块的分类】应用于乳腺癌诊断中的快速相反权重学习规则(Matlab代码实现)

内容概要:本文详细介绍了基于Plecs软件构建的全桥LLC谐振变换器仿真模型,重点阐述了其在软开关技术条件下的建模方法与仿真分析过程。该模型能够精确模拟LLC谐振变换器在不同运行工况下的动态响应特性,涵盖开环与闭环控制策略的设计与实现,适用于研究变换器的效率优化、谐振参数匹配、系统稳定性及软开关性能等问题。相较于Simulink等传统仿真工具,Plecs在高频电力电子系统仿真中展现出更高的仿真速度与数值精度,尤其适合用于复杂电力电子拓扑的快速原型验证与优化设计。; 适合人群:具备扎实电力电子技术基础和仿真经验,从事高频功率变换器设计、新能源电源开发、电力电子系统研究的研发工程师及高校研究生。; 使用场景及目标:①深入理解全桥LLC谐振变换器的工作原理与软开关实现机制;②掌握在Plecs环境中搭建高精度、高效率电力电子系统仿真模型的方法;③开展变换器控制策略(如变频控制、闭环稳压)的仿真验证与参数优化;④为科研课题、学位论文或工业项目提供可靠的仿真平台与技术支持。; 阅读建议:建议结合相关电力电子教材与LLC变换器设计指南,深入掌握谐振参数计算与系统稳定性分析方法,并通过反复调试仿真模型,观察关键波形(如原边电流零点切换、副边整流波形),以全面理解软开关过程的物理本质与控制实现细节。

C# Stopwatch类用法

//首先引入命名空间 using System.Diagnostics; public static void Mian() { Stopwatch sw = new Stopwatch(); sw.Start(); for (int i = 0; i < 1000000001; i++)

1148

产业园区运营负责人如何提升园区内技术成果与企业需求的精准对接效率?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

C# WiFi多线程收发报文

网络通信 public class NetClient { /// <summary> /// 接收数据缓冲区大小64K /// </summary> public const int DefaultBufferSize = 64 * 1024; public event Even...

750

国央企创新负责人如何利用数智平台提升核心技术攻关与成果转化协同能力?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

高校技术转移办公室人员如何高效完成技术成果的筛选与推介?.docx

科易网基于AI模型+行业大数据,在技术转移、成果转化、技术经纪、知识产权、产业创新、科技招商等垂直领域的多样化应用场景,推出科技创新领域的AI+工具与数智化解决方案,推动科技创新与产业创新智能化发展。

.NET Core 控制台程序在 K8s 上的优雅停机(graceful shutdown):没有“流量”可摘时,该怎么做?

.NET Core 控制台程序在 K8s 上的优雅停机,核心在于“停止新任务、妥善处理当前任务、主动退出”。针对不同任务类型:短任务可立即中断;有副作用的任务应等待完成;长任务则需通过 checkpoint 保存进度后退出。.NET Generic Host 自动处理 SIGTERM,通过 stoppingToken 响应停机信号。K8s 配置中 terminationGracePeriodSeconds 为硬上限,preStop 的 sleep 仅提供短暂缓冲窗口,真正关键在于应用层的精细化控制与任务状态

guigenyi的专栏 259

分布式推理服务(策略:数据并行、张量并行、流水线并行、专家并行)技术分析

摘要:分布式推理突破单GPU内存和算力限制,支持大模型部署。四种核心策略包括: 数据并行:复制模型拆分数据,提升吞吐但内存效率低; 流水线并行:模型分阶段执行,支持超大模型但存在计算气泡; 张量并行:拆分层内矩阵运算,延迟低但通信开销大; 专家并行:动态激活MoE模型专家,计算高效但路由复杂。实际应用中常组合策略(如3D并行),需权衡模型规模、硬件条件与性能目标。

技术引领业务创新 943
上一篇: C# 控制TSC打印机功能
code_long
博客等级 码龄16年 35粉丝 19原创
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值