欢迎光临
我们一直在努力

SketchCAD 打印样式映射:黑白出图的线宽分组与灰度正交设计

SketchCAD 打印样式映射:黑白出图的线宽分组与灰度正交设计

摘要

黑白出图的正确性并非“把彩色转成灰色”这一简单操作所能保证。本文围绕 SketchCAD 打印样式映射的设计与实现,揭示“彩色线宽图打印必错”的真实因果链:颜色与线宽在屏幕上构成冗余信息载体,黑白输出使颜色维度归零,若线宽分组未按 GB/T 14665 显式给定,则仅存的线宽载体退化为图元自动派生值,分组效果失控。由此提出核心命题——灰度化只负责去掉颜色,线宽分组必须由样式映射表显式指定,二者正交。

在数据结构层面,本文给出“三维匹配 + 三正交属性”的样式条目模型、图层 > 颜色 > 线型的优先级短路语义、GB 预设按线型分粗 / 细两档、简化 CTB 行式文本格式与宽容解析策略;在打印集成层面,给出网点抖动仅作用于填充面、网点尺寸锚定设备空间、DPI 与 painter 生命周期对齐、纸张方向与版面尺寸分离等关键设计。验证方面,A4 / A3 双路回归以彩色像素计数、墨量变化、灰度直方图 L1 差异三项指标判定,彩色像素 0 vs 15471、墨量 +19.1%、L1 1.38% 三项读数共同证明:样式表真实改变了出图且黑白化彻底。

关键词:打印样式映射;CTB;GB/T 14665;线宽分组;灰度正交;抖动画刷;DPI 对齐;回归基线


文章目录

  • SketchCAD 打印样式映射:黑白出图的线宽分组与灰度正交设计
    • 摘要
    • @[toc]
    • 1. 问题的真正根因:不是「颜色错了」,而是「分组信息载体错了」
      • 1.1 因果链分析
      • 1.2 核心命题
      • 1.3 因果图
    • 2. 数据结构:三个匹配维度 + 三个正交属性
      • 2.1 条目 PlotStyleEntry
      • 2.2 向后兼容的三个「不动点」
      • 2.3 匹配维度:图层 > 颜色 > 线型
      • 2.4 灰度化不改线宽
    • 3. GB 预设与简化 CTB
      • 3.1 gbPreset(LineWidthGroup):按线型分粗/细档
      • 3.2 简化 CTB 文本格式
    • 4. 七个真实踩坑(都是自测抓出来的,不是设想出来的)
      • 4.1 QLocale::c().toDouble("0.70") 对合法字符串回报失败
      • 4.2 `#` 既是注释符又是颜色前缀
      • 4.3 `continue` 只跳出内层循环,半成品条目被写入
      • 4.4 附带:QPixmap::setPixel 不可用
      • 4.5 样张工具:QImage 设备上的双重 painter + DPI 失配
      • 4.6 对照组基线选错会让回归测试永远「通过」
      • 4.7 QPageSize::size() 不含纸张方向,样张右侧被裁掉 87mm
    • 5. 打印链接入:抖动画刷与设备无关的网点尺寸
      • 5.1 调用点
      • 5.2 抖动仅作用于填充面
      • 5.3 网点尺寸必须锚在设备空间
    • 6. UI 与 COM 透传
      • 6.1 页面设置
      • 6.2 COM 批量出图
    • 7. 验收对照
      • 7.1 编译与自测
      • 7.2 A4/A3 黑白样张(目检)
      • 7.3 双路打印回归比对
    • 8. 创新点与经验沉淀
      • 8.1 创新点
      • 8.2 经验沉淀
    • 9. 未来工作
    • 9. 未来工作
    • 10. 结论
    • 附录 A:符号表
    • 附录 B:核心公式速查
    • 附录 C:交付物清单

1. 问题的真正根因:不是「颜色错了」,而是「分组信息载体错了」

1.1 因果链分析

“彩色线宽图打印必错”这句话容易把人引向“把颜色映射成黑色”这个方向,但那只解决了一半。真正的因果链是:

  • 屏幕上区分“粗实线 / 细实线 / 虚线 / 点画线”时,用户往往同时使用两种载体:颜色(红 / 蓝 / 绿 / 黄)与线宽(粗档 / 细档);
  • 彩色显示器上两者都在,信息冗余,怎么画都不会产生歧义;
  • 黑白打印机 / 黑白 PDF 上,颜色这一整个维度瞬间归零——红和绿都变成同一个中灰,原本靠颜色区分的线条在纸上完全无法分辨;
  • 若此时线宽又没有按 GB/T 14665 的粗 / 细两档显式给定,那么“线宽分组”这一唯一幸存的载体就退化成图元的自动派生线宽(defaultWidthForStyle),它由线型决定、与图幅线组有关但不受出图意图控制——于是“分组效果”不可控,表现为“打印必错”。
  • 1.2 核心命题

    本设计的核心命题可形式化为:

     灰度化

    ⊥

    线宽分组 

    \\boxed{\\ \\text{灰度化} \\perp \\text{线宽分组}\\ }

     灰度化⊥线宽分组 ​

    其中

    ⊥

    \\perp

    ⊥ 表示正交。即:

    灰度化只负责“去掉颜色”,线宽分组必须由样式映射表显式指定。 二者是正交的两件事,绝不能用“转灰度”顺带“猜”线宽。

    1.3 因果图

    形式化地,设:

    • C

      C

      C:颜色维度;

    • W

      W

      W:线宽维度;

    • B

      W

      \\mathrm{BW}

      BW:黑白输出条件;

    • O

      K

      \\mathrm{OK}

      OK:出图正确。

    在彩色输出下:

    C

    ≠

    ∅

    ∧

    W

    ≠

    ∅

    ⇒

    O

    K

    C \\neq \\emptyset \\land W \\neq \\emptyset \\Rightarrow \\mathrm{OK}

    C=∅∧W=∅⇒OK

    在黑白输出下:

    B

    W

    ⇒

    C

    =

    ∅

    \\mathrm{BW} \\Rightarrow C = \\emptyset

    BW⇒C=∅

    若

    W

    W

    W 未显式给定,则:

    C

    =

    ∅

    ∧

    W

    =

    W

    derived

    ⇒

    ¬

    O

    K

    C = \\emptyset \\land W = W_{\\text{derived}} \\Rightarrow \\neg \\mathrm{OK}

    C=∅∧W=Wderived​⇒¬OK

    因此修复路径必须同时满足:

    C

    =

    ∅

    ∧

    W

    =

    W

    explicit

    ⇒

    O

    K

    C = \\emptyset \\land W = W_{\\text{explicit}} \\Rightarrow \\mathrm{OK}

    C=∅∧W=Wexplicit​⇒OK

    这条命题直接决定了后文所有数据结构与不变式。


    2. 数据结构:三个匹配维度 + 三个正交属性

    2.1 条目 PlotStyleEntry

    在原有 overrideColor / overrideWidth 之上新增三个字段:

    字段含义JSON key旧档缺省
    overrideGray 是否强制灰度化 overrideGray false
    gray 目标灰度 $0 = $ 黑,$1 = $ 白 gray 0.0
    dither 灰度化后是否网点抖动 dither false

    2.2 向后兼容的三个「不动点」

    tests 中逐条断言:

  • 旧档 JSON 无这三个 key

    →

    \\to

    → fromJson 用 value(…).toBool(false) / toDouble(0.0) 兜底;

  • 兜底后 overrideGray = false

    →

    \\to

    → resolve() 不做灰度化

    →

    \\to

    → 输出颜色与旧版逐字相同;

  • 未选样式表(plotStyle.empty() 且 monochrome = false)

    →

    \\to

    → effectivePlotStyle() 返回空表

    →

    \\to

    → 所有 resolve() 走“沿用图元自身属性”分支

    →

    \\to

    → 仍是既有 G3 线宽组默认行为。

  • 也就是说,本次改动对“不用样式表的用户”是零行为变更,这是硬约束“未选择样式表时保持现有 G3 线宽组默认行为不变”的技术兑现方式。

    形式化地,设旧版输出为

    O

    0

    O_0

    O0​,新版输出为

    O

    1

    O_1

    O1​:

    无样式表

    ∧

    ¬

    m

    o

    n

    o

    c

    h

    r

    o

    m

    e

    ⇒

    O

    1

    =

    O

    0

    \\text{无样式表} \\land \\neg \\mathrm{monochrome} \\Rightarrow O_1 = O_0

    无样式表∧¬monochrome⇒O1​=O0​

    2.3 匹配维度:图层 > 颜色 > 线型

    原有实现只有两个维度(图层、颜色)。GB 黑白预设需要按线型分组(粗实 / 细实 / 虚线 / 点画线),因为国标的粗细分类本身就是按线型定义的。故新增第三张表 byStyle_:

    QMap<QString, PlotStyleEntry> byLayer_; // 图层名 → 条目
    QMap<int, PlotStyleEntry> byColor_; // QColor::rgba() 整数键 → 条目
    QMap<int, PlotStyleEntry> byStyle_; // (int)LineStyle → 条目

    resolve() 采用首个命中即返回(不再向下匹配):

    const PlotStyleEntry* hit = nullptr;
    auto lit = byLayer_.find(el.layer);
    if (lit != byLayer_.end()) hit = &lit.value();
    else { /* 颜色 */ } else { /* 线型 */ }

    优先级可形式化定义为:

    resolve

    ⁡

    (

    e

    )

    =

    {

    E

    layer

    (

    e

    .

    l

    a

    y

    e

    r

    )

    ,

    若存在

    ,

    E

    color

    (

    e

    .

    c

    o

    l

    o

    r

    )

    ,

    若存在且无图层条目

    ,

    E

    style

    (

    e

    .

    s

    t

    y

    l

    e

    )

    ,

    若存在且无颜色条目

    ,

    default

    ,

    否则

    .

    \\operatorname{resolve}(e) = \\begin{cases} E_{\\text{layer}}(e.\\mathrm{layer}), & \\text{若存在}, \\\\ E_{\\text{color}}(e.\\mathrm{color}), & \\text{若存在且无图层条目}, \\\\ E_{\\text{style}}(e.\\mathrm{style}), & \\text{若存在且无颜色条目}, \\\\ \\text{default}, & \\text{否则}. \\end{cases}

    resolve(e)=⎩

    ⎨

    ⎧​Elayer​(e.layer),Ecolor​(e.color),Estyle​(e.style),default,​若存在,若存在且无图层条目,若存在且无颜色条目,否则.​

    为什么是「首个命中即返回」而不是「逐维度叠加合并」? 叠加语义会让“图层给宽度、颜色给灰度”这类组合产生难以预期的结果(用户无法预测最终线宽),而优先级短路的语义可预测性更强:用户一旦为某图层设了条目,就完全掌控它,不受图元颜色干扰。这与 AutoCAD CTB“先按对象再按颜色”的短路思路一致。

    2.4 灰度化不改线宽

    if (r.gray) r.color = plotGrayscale(r.color, r.grayValue);
    // 注意:此行之后 widthMm 原封不动

    plotGrayscale 是纯函数:

    const int v = int(std::lround(g * 255.0)); // g=0 → 0(黑),g=1 → 255(白)
    return QColor(v, v, v, c.alpha()); // 保留 alpha

    灰度化公式为:

    v

    =

    c

    l

    a

    m

    p

    (

    r

    o

    u

    n

    d

    (

    g

    ×

    255

    )

    ,

     

    0

    ,

     

    255

    )

    v = \\mathrm{clamp}\\left(\\mathrm{round}(g \\times 255),\\ 0,\\ 255\\right)

    v=clamp(round(g×255), 0, 255)

    其中

    g

    ∈

    [

    0

    ,

    1

    ]

    g \\in [0, 1]

    g∈[0,1] 为目标灰度,

    v

    v

    v 为输出 RGB 通道值。输出颜色为:

    R

    G

    B

    out

    =

    (

    v

    ,

    v

    ,

    v

    )

    \\mathrm{RGB}_{\\text{out}} = (v, v, v)

    RGBout​=(v,v,v)

    Alpha 通道保留原值:

    α

    out

    =

    α

    in

    \\alpha_{\\text{out}} = \\alpha_{\\text{in}}

    αout​=αin​

    自测中专门断言“灰度化后 widthMm 不变”,把这条不变式固化,防止后来者“顺手”在灰度分支里也去改线宽——那正是“彩色线宽图打印必错”的另一种复现。


    3. GB 预设与简化 CTB

    3.1 gbPreset(LineWidthGroup):按线型分粗/细档

    const double thick = groupThickMm(group); // G3 → 1.0
    const double thin = groupThinMm(group); // G3 → 0.5
    t.setStyleStyle(LineStyle::Solid, mk(thick)); // 粗实线
    t.setStyleStyle(LineStyle::ThickDashed, mk(thick)); // 粗虚线
    t.setStyleStyle(LineStyle::ThickDashDot, mk(thick)); // 粗点画线
    t.setStyleStyle(LineStyle::Dashed, mk(thin)); // 细虚线
    ... 其余细线型同样 mk(thin)

    其中 mk(w) 统一设 overrideGray = true, gray = 0, dither = false——线宽图要锐利,不抖动。粗/细档直接取自 LineWidthGroup.h(GB/T 14665-2012 表 1,粗:细恒

    2

    :

    1

    2:1

    2:1),预设本身不硬编码任何线宽数值,因此切换图幅线组(G1…G5)后预设自动跟随。

    粗细比公式:

    r

    thick:thin

    =

    w

    thick

    w

    thin

    =

    2

    r_{\\text{thick:thin}} = \\frac{w_{\\text{thick}}}{w_{\\text{thin}}} = 2

    rthick:thin​=wthin​wthick​​=2

    g

    b

    P

    r

    e

    s

    e

    t

    G

    r

    a

    y

    D

    i

    t

    h

    e

    r

    (

    )

    gbPresetGrayDither()

    gbPresetGrayDither() 在此基础上开全局灰度+抖动,用于剖面/填充等需要中间调层次的场景。

    3.2 简化 CTB 文本格式

    真实 AutoCAD CTB 是二进制,本项目取其“行式文本子集”,便于版本管理/审阅/交付:

    # 注释
    LAYER 轮廓 WIDTH=0.70 GRAY=0.0
    COLOR #FF0000 WIDTH=0.35 DITHER=1
    STYLE DASHED WIDTH=0.50
    GRAYSCALE 0.0 DITHER=1

    fromCtb() 采用宽容解析:

    • 未知关键字/非法数值

      →

      \\to

      → 记录错误、跳过该行,已解析的行照常生效;

    • 行内任一 token 出错

      →

      \\to

      → 整行丢弃(不写半成品条目);

    • 返回 false 并给出第一条错误串,供 UI 弹窗提示。

    toCtb() 为其逆运算,便于把现场样式表导出给第三方或纳入存档审阅。

    CTB 文法的形式化定义为:

    Line

    →

    Directive 

    Token

    ∗

    \\text{Line} \\to \\text{Directive}\\ \\text{Token}^*

    Line→Directive Token∗

    Directive

    ∈

    {

    LAYER

    ,

    COLOR

    ,

    STYLE

    ,

    GRAYSCALE

    }

    \\text{Directive} \\in \\{\\text{LAYER}, \\text{COLOR}, \\text{STYLE}, \\text{GRAYSCALE}\\}

    Directive∈{LAYER,COLOR,STYLE,GRAYSCALE}

    Token

    →

    Key

    =

    Value

    \\text{Token} \\to \\text{Key} = \\text{Value}

    Token→Key=Value

    Key

    ∈

    {

    WIDTH

    ,

    GRAY

    ,

    DITHER

    }

    \\text{Key} \\in \\{\\text{WIDTH}, \\text{GRAY}, \\text{DITHER}\\}

    Key∈{WIDTH,GRAY,DITHER}


    4. 七个真实踩坑(都是自测抓出来的,不是设想出来的)

    这一节记录本次实现中由自测断言暴露、并最终修复的七个缺陷。它们都属于“看起来能跑、实际全错”的类别,值得单独留档。

    4.1 QLocale::c().toDouble(“0.70”) 对合法字符串回报失败

    现象:CTB 导入全部失败,提示 WIDTH 非法: 0.70,而打印出的字符码是 48, 46, 55, 48(即 "0.70",完全合法)。

    排查过程:写独立探针程序编译链接同样的 Qt 6.10.1 / MinGW 13.1,发现三种写法在探针里全部成功:QLocale::c().toDouble / QString::toDouble / QString::fromLatin1().toDouble。再回到工程内联打印,发现 QLocale::c().toDouble 返回了正确的数值却把 ok 置为 false,而同一位置用 QString::toDouble 解析同一字符串则 ok = true。

    结论:在本工具链组合下 QLocale::c()(C 区域、language 为空)这一路径的 ok 回报不可信。修复:自写 ctbToDouble() 作为全工程唯一数值入口,先把 , 归一为 .(抵御 de_DE 类逗号小数区域),再走 QString::toDouble:

    static bool ctbToDouble(const QString& s, double* out) {
    QString t = s.trimmed();
    t.replace(QLatin1Char(','), QLatin1Char('.'));
    bool ok = false;
    const double v = t.toDouble(&ok);
    if (ok && out) *out = v;
    return ok;
    }

    教训:“解析成功与否”和“解析出的值”必须分别验证。只看 ok 会被区域设置坑,只看值又会被静默失败坑。

    4.2 # 既是注释符又是颜色前缀

    最初的行尾注释处理是 ln.left(ln.indexOf('#')),这会把 COLOR #FF0000 WIDTH=0.35 从中间截断成 COLOR,导致颜色映射整条失效。

    修复规则:扫描每个 #,统计其后连续十六进制字符数,长度为 3 / 6 / 8 视为颜色 token(#RGB / #RRGGBB / #AARRGGBB),否则视为注释起点。

    形式化判据:

    isColorToken

    (

    #

    s

    )
      

    ⟺
      

    ∣

    s

    ∣

    ∈

    {

    3

    ,

    6

    ,

    8

    }

    ∧

    s

    ∈

    H

    e

    x

    ∗

    \\text{isColorToken}(\\#s) \\iff |s| \\in \\{3, 6, 8\\} \\land s \\in \\mathrm{Hex}^*

    isColorToken(#s)⟺∣s∣∈{3,6,8}∧s∈Hex∗

    4.3 continue 只跳出内层循环,半成品条目被写入

    for (int i = 2; …) 的 token 循环里遇到非法参数执行 continue,看似跳过了该 token,但外层的 setLayerStyle(target, e) 仍会执行,于是一个 WIDTH=abc 的条目被以默认值(0.25mm / 不灰度)写入表中——比直接报错更危险,因为用户看不出样式表已被污染。

    修复:引入 lineBad 标志,循环结束后 if (lineBad) continue; 整行丢弃。

    4.4 附带:QPixmap::setPixel 不可用

    makeDitherBrush 最初用 QPixmap::setPixelColor / setPixel 写网点贴图,本工程编译单元报 no member named 'setPixel'。改用 QImage(Format_RGB32)逐像素写入后再 QPixmap::fromImage(),同时天然规避了“设备无关位图不能直接读写像素”的 Qt 约束。

    4.5 样张工具:QImage 设备上的双重 painter + DPI 失配

    生成双路回归图时首版输出全白(两路 PNG 字节数完全相同、彩色像素 0)。两个叠加原因:

  • 双重 painter:先 QPainter p(&img),再调 PrintEngine::render(&m, &img, s),而后者内部自行 QPainter p(dev)——同一设备已有活动 painter,begin() 失败,render 直接返回,图像保持初始白底。修复:不再提前激活 painter。
  • DPI 失配:PrintEngine::devicePxPerMm() 读 dev->logicalDpiX(),而 QImage 默认为 96dpi。按 300dpi 计算画布尺寸、引擎按 96dpi 换算可打印区,两者不一致。修复:img.setDotsPerMeterX/Y(300/0.0254) 显式声明 DPI。
  • DPI 换算公式为:

    p

    x

    P

    e

    r

    M

    m

    =

    D

    P

    I

    25.4

    \\mathrm{pxPerMm} = \\frac{\\mathrm{DPI}}{25.4}

    pxPerMm=25.4DPI​

    当 DPI 从

    96

    96

    96 变为

    300

    300

    300 时:

    p

    x

    P

    e

    r

    M

    m

    (

    300

    )

    p

    x

    P

    e

    r

    M

    m

    (

    96

    )

    =

    300

    96

    =

    3.125

    \\frac{\\mathrm{pxPerMm}(300)}{\\mathrm{pxPerMm}(96)} = \\frac{300}{96} = 3.125

    pxPerMm(96)pxPerMm(300)​=96300​=3.125

    即画布尺寸按

    300
     

    D

    P

    I

    300\\,\\mathrm{DPI}

    300DPI 计算、可打印区按

    96
     

    D

    P

    I

    96\\,\\mathrm{DPI}

    96DPI 计算,两者相差

    3.125

    3.125

    3.125 倍。

    这两条都属于“离线出图工具”与“真实打印设备(QPdfWriter 走 300dpi)”之间的环境差异,提醒我们:用 QImage 充当打印设备做回归时,必须显式对齐 DPI 与 painter 生命周期。

    4.6 对照组基线选错会让回归测试永远「通过」

    首版对照组设了 monochrome = true 想表达“仅灰度”,但 effectivePlotStyle() 的语义是“monochrome 且样式表为空

    →

    \\to

    → 自动套 GB 预设”——于是对照组也拿到了同一份 GB 预设,两路输出逐像素相同,

    L

    1

    =

    0

    \\mathrm{L1} = 0

    L1=0,回归测试形式上“无差异”。

    修复:对照组必须是完全不带样式表、不带 monochrome 的原始彩色出图,即真正复现“彩色线宽图直接打到黑白打印机”这个错误基线。

    教训:回归测试的对照组必须精确对应“修复前的真实行为”。用“另一个也开了修复开关的配置”当对照组,会得到“无差异”的假通过。断言 PASS 之前,先确认它在修复前会 FAIL。

    4.7 QPageSize::size() 不含纸张方向,样张右侧被裁掉 87mm

    目检 gray_A4_style.png 时发现第 4 列线段与外框右边完全不可见,只剩 3 列。初步判断是样张几何参数画得太宽(模型 225 mm vs A4 可打印 190 mm),于是把线段间距从

    k

    ⋅

    60

    +

    45

    k \\cdot 60 + 45

    k⋅60+45 收到

    k

    ⋅

    50

    +

    40

    k \\cdot 50 + 40

    k⋅50+40、外框从

    (

    −

    15

    ,

    18

    )

    ∼

    (

    210

    ,

    −

    100

    )

    (-15, 18) \\sim (210, -100)

    (−15,18)∼(210,−100) 收到

    (

    −

    12

    ,

    16

    )

    ∼

    (

    205

    ,

    −

    100

    )

    (-12, 16) \\sim (205, -100)

    (−12,16)∼(205,−100)——重编译后裁切依旧,说明根因不在几何。

    真正原因在样张工具的画布构造:

    const QSizeF szMm = size.size(QPageSize::Millimeter); // A4 → 210×297(纵向!)
    const int W = int(szMm.width() * pxPerMm); // 建出纵向画布
    const int H = int(szMm.height() * pxPerMm);

    QPageSize::size() 返回的是纸张固有尺寸(永远是纵向基态),与 orientation 无关。而 PrintEngine::render() 内部走 effectivePageW/H(),横向下按

    297

    ×

    210

    297 \\times 210

    297×210 排版:

    return (s.orientation == QPageLayout::Landscape) ? ps.height() : ps.width();

    于是画布 210 mm 宽、排版 297 mm 宽,右侧

    87
     

    m

    m

    87\\,\\mathrm{mm}

    87mm 直接落在画布外被裁。PNG 尺寸

    780

    ×

    1054

    780 \\times 1054

    780×1054(纵向)本身就是最直接的证据——而设置里明明写的是 Landscape。

    修复:画布尺寸按方向交换宽高,与 effectivePageW/H() 保持同一套换算。

    const double pageWmm = (s.orientation == QPageLayout::Landscape) ? szMm.height() : szMm.width();
    const double pageHmm = (s.orientation == QPageLayout::Landscape) ? szMm.width() : szMm.height();

    修复后 PNG 变为

    3507

    ×

    2480

    3507 \\times 2480

    3507×2480(横向),4 列线段与外框四边完整闭合。

    教训:QPageSize::size() 是纸张尺寸,QPageLayout 才是版面尺寸,两者差一个方向换算。凡是“离屏设备尺寸”与“打印设置”两处独立计算的地方,都必须复用引擎内部的换算函数,否则必然出现静默裁切。这类 bug 不报错、只丢内容——第一反应怀疑几何参数是本能误导,必须先看输出图的宽高比是否与设置的方向一致。注意 PDF 路径不受影响:QPdfWriter::setPageOrientation() 自己处理了方向,所以只有离屏 QImage 回归图会错,这也解释了为何 PDF 样张一直是好的。


    5. 打印链接入:抖动画刷与设备无关的网点尺寸

    5.1 调用点

    // PrintEngine.cpp
    PlotRenderer::renderModel(&p, model, scaleDev, clipWorld,
    s.effectivePlotStyle(), s.lineWidthScale);

    灰度信息经 resolve() 已经在 rs.color 中了,所有 paint* 分支(线 / 剖面 / 标注 / 文字 / 块引用 / 表格 / 边界)因为统一使用 rs.color 而自动继承灰度化,无需逐处改动。这是把“颜色决策”收敛到 resolve() 一处的收益。

    调用链可形式化为:

    Model

    →

    resolve

    ⁡

    {

    (

    e

    ,

    r

    s

    )

    }

    →

    render

    ⁡

    Device

    \\text{Model} \\xrightarrow{\\operatorname{resolve}} \\{(e, \\mathrm{rs})\\} \\xrightarrow{\\operatorname{render}} \\text{Device}

    Modelresolve

    ​{(e,rs)}render

    ​Device

    所有绘制分支共享同一 rs.color,因此颜色决策单点生效。

    5.2 抖动仅作用于填充面

    if (rs.gray && rs.dither) {
    p->setBrush(makeDitherBrush(rs.grayValue, c.scaleDev)); // 网点
    } else {
    p->setBrush(rs.color); // 实灰度
    }

    只接在 paintHatch 的 SolidFill(狭小区域涂黑)分支:线宽图的轮廓线若抖动会变毛、失去锐度,而面状填充抖动才有意义(恢复中间灰层次)。这是按“笔画 vs 填充”区分抖动的必要性。

    5.3 网点尺寸必须锚在设备空间

    QBrush 的贴图 tile 默认随世界变换缩放。图幅放大时网点会跟着变粗、退化成灰块。修复:

    br.setTransform(QTransform::fromScale(1.0/scaleDev, 1.0/scaleDev));

    抵消世界变换,使网点在纸面上恒为固定物理尺寸(默认 4 设备像素)。

    网点密度定义为黑点数占比:

    ρ

    black

    =

    1

    −

    g

    \\rho_{\\text{black}} = 1 – g

    ρblack​=1−g

    其中

    g

    g

    g 为目标灰度。当

    g

    =

    0

    g = 0

    g=0 时

    ρ

    black

    =

    1

    \\rho_{\\text{black}} = 1

    ρblack​=1(全黑),当

    g

    =

    1

    g = 1

    g=1 时

    ρ

    black

    =

    0

    \\rho_{\\text{black}} = 0

    ρblack​=0(全白),方向与“

    g

    g

    g 越大越浅”一致。

    网点 tile 尺寸为:

    t

    tile

    =

    4

     设备像素

    t_{\\text{tile}} = 4\\ \\text{设备像素}

    ttile​=4 设备像素

    经设备空间锚定后,纸面上的物理尺寸为:

    t

    paper

    =

    t

    tile

    p

    x

    P

    e

    r

    M

    m

    =

    4

    p

    x

    P

    e

    r

    M

    m

     

    m

    m

    t_{\\text{paper}} = \\frac{t_{\\text{tile}}}{\\mathrm{pxPerMm}} = \\frac{4}{\\mathrm{pxPerMm}}\\ \\mathrm{mm}

    tpaper​=pxPerMmttile​​=pxPerMm4​ mm


    6. UI 与 COM 透传

    6.1 页面设置

    PrintSettingsDialog 的“打印样式(简化 CTB)”组新增:

    • 样式表预设下拉:无 / GB 黑白出图 / GB 灰度抖动 / 自定义;
    • 导入 CTB… 按钮:读文件

      →

      \\to

      → fromCtb

      →

      \\to

      → 解析有瑕疵时弹“已跳过哪些行”;

    • 黑白出图(打印机 / PDF) 总开关:写入 PrintSettings::monochrome;
    • 匹配维度加**“按线型”**(弹中文列表,内部转 plotStyleName);
    • 条目编辑加灰度 / 抖动两个控件;
    • 列表项描述附灰度后缀(如 灰度0.30(抖动)),目检前无需点开每条就能确认黑白意图。

    monochrome 与预设的分工:effectivePlotStyle() 里只有当表为空时才套 GB 预设,用户手工建过表就以手工表为准——避免“勾了黑白开关就把精心设的线宽分组抹掉”。

    形式化地:

    effectivePlotStyle

    ⁡

    (

    s

    e

    t

    t

    i

    n

    g

    s

    )

    =

    {

    G

    B

    P

    r

    e

    s

    e

    t

    ,

    若 

    m

    o

    n

    o

    c

    h

    r

    o

    m

    e

    ∧

    s

    t

    y

    l

    e

    T

    a

    b

    l

    e

    =

    ∅

    ,

    s

    t

    y

    l

    e

    T

    a

    b

    l

    e

    ,

    若 

    s

    t

    y

    l

    e

    T

    a

    b

    l

    e

    ≠

    ∅

    ,

    ∅

    ,

    否则

    .

    \\operatorname{effectivePlotStyle}(\\mathrm{settings}) = \\begin{cases} \\mathrm{GBPreset}, & \\text{若 } \\mathrm{monochrome} \\land \\mathrm{styleTable} = \\emptyset, \\\\ \\mathrm{styleTable}, & \\text{若 } \\mathrm{styleTable} \\neq \\emptyset, \\\\ \\emptyset, & \\text{否则}. \\end{cases}

    effectivePlotStyle(settings)=⎩

    ⎨

    ⎧​GBPreset,styleTable,∅,​若 monochrome∧styleTable=∅,若 styleTable=∅,否则.​

    6.2 COM 批量出图

    BatchTask 新增两个字段,exportDrawing 加同名默认参(ABI 友好):

    QString ctbText; // 简化 CTB 文本
    QString preset; // "gb" | "gbGray" | "none"

    任务 JSON 里支持 "styleTable" 字段,自动判别是预设名还是 CTB 文本(含换行或 LAYER / COLOR / STYLE / GRAYSCALE 关键字

    →

    \\to

    → CTB 文本)。ctbText 优先于 preset。

    判别规则可形式化:

    isCtb

    ⁡

    (

    s

    )
      

    ⟺
      

    (

    s

    ∋

    \\n

    )

    ∨

    (

    s

    ∋

    {

    LAYER

    ,

    COLOR

    ,

    STYLE

    ,

    GRAYSCALE

    }

    )

    \\operatorname{isCtb}(s) \\iff \\left( s \\ni \\texttt{\\textbackslash n} \\right) \\lor \\left( s \\ni \\{\\texttt{LAYER}, \\texttt{COLOR}, \\texttt{STYLE}, \\texttt{GRAYSCALE}\\} \\right)

    isCtb(s)⟺(s∋\\n)∨(s∋{LAYER,COLOR,STYLE,GRAYSCALE})

    外部系统无需手工拼整份 PrintSettings JSON 即可为每张图指定黑白线宽 / 灰度。


    7. 验收对照

    7.1 编译与自测

    项目结果
    SketchCAD 构建 EXIT=0,-Wall -Wextra 零警告
    SketchSelfTest 构建 EXIT=0,零警告
    SketchBenchmark 构建 EXIT=0,零警告
    PlotStyleSamples 构建 EXIT=0,零警告
    自测用例 通过 344 / 失败 0(315 → 344,新增 t_plotStyle())

    315

    →

    344

    315 \\to 344

    315→344 的 29 条新断言覆盖:映射解析(颜色

    →

    \\to

    → 线宽)、优先级(图层 > 颜色 > 线型)、灰度纯函数(

    0

    /

    0.5

    /

    1

    0 / 0.5 / 1

    0/0.5/1 三点)、灰度不改线宽、GB 预设粗细分组(G3:

    1.0

    /

    0.5

    1.0 / 0.5

    1.0/0.5)、CTB 四关键字解析、CTB 宽容容错、JSON 往返、旧档缺键兼容。

    7.2 A4/A3 黑白样张(目检)

    样张由 PlotStyleSamples 生成,四类线型各一行、每行四条同色线,刻意用四个明显不同的彩色以复现“彩色线宽图”场景:

    文件用途
    bw_A4_style.pdf / bw_A3_style.pdf 带 GB 样式表的黑白样张(待目检签字)
    bw_A4_nostyle.pdf / bw_A3_nostyle.pdf 对照组:完全不带样式表、不带 monochrome 的原始彩色出图
    gray_A4_style.png / gray_A4_nostyle.png 双路回归用的灰度位图(同一条 PrintEngine::render 链路)
    regression.txt 双路灰度直方图 / 墨量 / 彩色像素比对报告

    对照组刻意取“原始彩色 + 自动派生线宽”,即修复前彩色线宽图直接打印的真实行为;若对照组也开 monochrome,effectivePlotStyle() 会给它套同一份 GB 预设,两路将逐像素相同,回归测试就成了假通过(详见 4.6)。

    目检要点(对照 GB/T 14665):

    • 粗实线:最粗且纯黑;
    • 细实线:约为粗线一半宽、纯黑;
    • 虚线:细档、线段间断均匀;
    • 点画线:细档、长短段交替。

    签字确认由人工在打开 PDF 后完成(本记录为样张产出与自动化回归部分)。

    7.3 双路打印回归比对

    PlotStyleSamples 对“带样式表 / 不带样式表”两路各渲染一份灰度图(复用同一条 PrintEngine::render 链路,故比对的即是真实出图路径),统计:

    • 平均灰度

      g

      ˉ

      \\bar{g}

      gˉ​;

    • 总墨量

      Σ

      (

      255

      −

      g

      )

      \\Sigma(255 – g)

      Σ(255−g) 及相对变化;

    • 256 桶灰度直方图的

      L

      1

      \\mathrm{L1}

      L1 差异;

    • 彩色像素计数(

      R

      ≠

      G

      ≠

      B

      \\mathrm{R} \\neq \\mathrm{G} \\neq \\mathrm{B}

      R=G=B 的抽样像素)——这是“黑白化是否彻底”的直接判据。

    判定:两路差异可测(样式表确实改变了出图)且带样式表组彩色像素为 0(黑白化正确)。

    关键指标形式化定义:

    平均灰度:

    g

    ˉ

    =

    1

    N

    ∑

    i

    =

    1

    N

    g

    i

    \\bar{g} = \\frac{1}{N} \\sum_{i=1}^{N} g_i

    gˉ​=N1​i=1∑N​gi​

    总墨量:

    M

    =

    ∑

    i

    =

    1

    N

    (

    255

    −

    g

    i

    )

    M = \\sum_{i=1}^{N} (255 – g_i)

    M=i=1∑N​(255−gi​)

    灰度直方图

    L

    1

    \\mathrm{L1}

    L1 差异:

    L

    1

    =

    ∑

    b

    =

    0

    255

    ∣

    H

    style

    (

    b

    )

    −

    H

    nostyle

    (

    b

    )

    ∣

    \\mathrm{L1} = \\sum_{b=0}^{255} \\left| H_{\\text{style}}(b) – H_{\\text{nostyle}}(b) \\right|

    L1=b=0∑255​∣Hstyle​(b)−Hnostyle​(b)∣

    彩色像素计数:

    N

    color

    =

    ∣

    {

    i

    ∣

    R

    i

    ≠

    G

    i

    ∨

    G

    i

    ≠

    B

    i

    }

    ∣

    N_{\\text{color}} = \\left| \\{ i \\mid R_i \\neq G_i \\lor G_i \\neq B_i \\} \\right|

    Ncolor​=∣{i∣Ri​=Gi​∨Gi​=Bi​}∣

    实测结果(A4 / A3 双 PASS,EXIT=0):

    指标A4 带样式表A4 不带A3 带样式表A3 不带
    平均灰度 251.16 251.78 253.06 253.36
    总墨量

    Σ

    (

    255

    −

    g

    )

    \\Sigma(255 – g)

    Σ(255−g)

    33 362 717 28 020 543 33 804 707 28 462 547
    墨量变化 +19.1% — +18.8% —
    灰度直方图

    L

    1

    \\mathrm{L1}

    L1

    119 626(1.3754%) 112 052(0.6442%)
    彩色像素(抽样) 0 15 471 0 14 522
    判定 PASS PASS

    三点关键读数:

  • 彩色像素

    0

    0

    0 vs

    15471

    15471

    15471 —— 样式表组彻底灰度化,对照组保留原始彩色。黑白化正确。

  • 墨量

    +

    19.1

    %

    +19.1\\%

    +19.1% /

    +

    18.8

    %

    +18.8\\%

    +18.8% —— 差异方向正确且量级合理。GB 预设把粗实线拉到

    1.0
     

    m

    m

    1.0\\,\\mathrm{mm}

    1.0mm、细线类压到

    0.5
     

    m

    m

    0.5\\,\\mathrm{mm}

    0.5mm(

    2

    :

    1

    2 : 1

    2:1 分组),而对照组四类线全是同一自动派生宽度。带样式表组着墨更多 = 线宽分组真实生效,而非仅仅“变成了灰色”。

  • L

    1

    \\mathrm{L1}

    L1 差异

    1.38

    %

    1.38\\%

    1.38% /

    0.64

    %

    0.64\\%

    0.64% —— 远超

    10

    −

    4

    10^{-4}

    10−4 判定阈值,不是噪声。

  • 目检结论(对 gray_A4_style.png / bw_A4_style.pdf):

    • 四列线段完整可见、外框四边闭合(横向

      3507

      ×

      2480

      3507 \\times 2480

      3507×2480,见 4.7 纸张方向修复);

    • 粗实线最粗且纯黑,细实线明显较细(约一半宽),虚线为细档间断,点画线为细档长短段交替;
    • 四类线型纯灰度下仍可凭线宽清晰区分 —— 即本任务要达成的核心目标。

    对照 gray_A4_nostyle.png 可见:四类线粗细完全一致,仅靠红 / 蓝 / 绿 / 黄区分。这正是“彩色线宽图打印必错”的现场:一旦落到黑白输出机或黑白 PDF,颜色维度归零,四类线将无法辨识。

    PDF 样张的人工签字确认仍待人工在阅读器中打开 bw_A4_style.pdf / bw_A3_style.pdf 后完成。


    8. 创新点与经验沉淀

    8.1 创新点

  • 灰度与线宽正交性形式化:以

    C

    =

    ∅

    ∧

    W

    =

    W

    explicit

    ⇒

    O

    K

    C = \\emptyset \\land W = W_{\\text{explicit}} \\Rightarrow \\mathrm{OK}

    C=∅∧W=Wexplicit​⇒OK 形式化表达黑白出图正确性条件,明确灰度与线宽是两个独立维度。这一形式化不仅澄清了“转灰度”与“线宽分组”的职责边界,还直接推导出后文全部数据结构与不变式,使设计从“经验直觉”上升为“可验证命题”。

  • 三维匹配 + 三正交属性模型:图层 / 颜色 / 线型三匹配维度,线宽 / 灰度 / 抖动三正交属性,层次清晰。三匹配维度覆盖了“按图层统一管理”与“按颜色 / 线型精细控制”两类使用习惯,三正交属性则保证任一属性可独立调整而不干扰其余属性,为后续扩展(如按对象类型映射)预留了清晰的挂载点。
  • 优先级短路语义:以“首个命中即返回”替代“逐维度叠加合并”,提升用户可预测性。叠加语义会让“图层给宽度、颜色给灰度”这类组合产生难以预期的结果,而优先级短路让用户一旦为某图层设了条目就完全掌控它,不受图元颜色干扰,与 AutoCAD CTB“先按对象再按颜色”的短路思路一致。
  • GB 预设按线型分组:粗实 / 粗虚 / 粗点画取粗档,细线类取细档,粗 : 细恒

    2

    :

    1

    2 : 1

    2:1,与 GB/T 14665-2012 对齐。预设本身不硬编码任何线宽数值,而是从 LineWidthGroup 动态取档,因此切换图幅线组(G1…G5)后预设自动跟随,避免了“换图幅就要改样式表”的维护负担。

  • 简化 CTB 行式文本子集:以可读文本替代二进制 CTB,便于版本管理与审阅;宽容解析采用“整行丢弃”策略,避免半成品条目污染。行式文本可直接纳入 Git 做 diff 审阅,宽容解析则保证“坏一行不坏全表”,兼顾健壮性与可诊断性。
  • 抖动仅作用于填充面:按“笔画 vs 填充”区分抖动必要性,线宽图轮廓保持锐利。这一区分源于对打印物理的观察:轮廓线抖动会变毛、失去锐度,而面状填充抖动才能恢复中间灰层次,是“按用途选算法”而非“一刀切”的典型。
  • 网点尺寸设备空间锚定:以 QTransform::fromScale(1/scaleDev, 1/scaleDev) 抵消世界变换,使网点纸面尺寸恒定。若不锚定,图幅放大时网点会跟着变粗、退化成灰块;锚定后网点在纸面上恒为固定物理尺寸,保证了不同缩放下的灰度观感一致。
  • 双路回归三指标判据:彩色像素计数、墨量变化、直方图

    L

    1

    \\mathrm{L1}

    L1 差异,共同判定黑白化彻底性与线宽分组有效性。三指标互为补充:彩色像素计数直接回答“黑白化是否彻底”,墨量变化回答“线宽分组是否真实生效”,直方图

    L

    1

    \\mathrm{L1}

    L1 回答“差异是否显著”,任一单指标都容易被“假通过”欺骗。

  • 对照组精确对应修复前行为:以原始彩色 + 自动派生线宽为基线,避免“假通过”。回归测试的对照组必须精确对应“修复前的真实行为”,用“另一个也开了修复开关的配置”当对照组会得到“无差异”的假通过——这是测试设计层面的通用教训。
  • 纸张尺寸 vs 版面尺寸分离:明确 QPageSize::size() 与 QPageLayout 的差异,避免离屏 QImage 静默裁切。这一发现揭示了“离屏设备尺寸”与“打印设置”两处独立计算必须复用同一套换算函数,否则必然出现静默裁切,且这类 bug 不报错只丢内容。
  • 8.2 经验沉淀

    • 把“颜色决策”收敛到 resolve() 一处,让所有渲染分支自动继承,是本次改动能只动一行调用点就覆盖全部图元类型的原因;分散决策必然漏改某一类图元。这一“单点决策”原则适用于任何跨多分支的横切属性(颜色、线宽、透明度等),是降低渲染管线耦合度的通用手法。
    • “灰度”与“线宽”正交是本任务最容易做错的地方。一旦让灰度分支顺手改线宽(或反过来让线宽映射隐含改灰度),就会产出“看起来是黑白图、但分组是乱的”这种更难排查的问题。正交性不仅是设计命题,更要在自测中用“灰度化后 widthMm 不变”这类不变式钉死,防止后来者“顺手”破坏。
    • 宽容解析必须有“整行丢弃”兜底:只跳过坏 token 会写出半成品条目,比严格报错更危险。半成品条目以默认值(0.25 mm / 不灰度)写入表中,用户看不出样式表已被污染,问题会潜伏到出图阶段才暴露。宽容解析的正确姿势是“坏行丢弃 + 已解析行生效 + 返回错误串供 UI 提示”。
    • ok 标志不可单独信任:本工程实测 QLocale::c().toDouble 返回真值却置 ok = false。涉及用户输入的数值解析,建议自写归一化包装并在自测中用“预期值”而非仅“ok”断言。“解析成功与否”和“解析出的值”必须分别验证,只看 ok 会被区域设置坑,只看值又会被静默失败坑。
    • 逗号 vs 句点小数点是 CTB / 配置文件解析的国际性坑点,归一化比依赖 QLocale 更稳。de_DE 类区域用逗号作小数分隔符,直接 toDouble 会静默失败或解析出错误值;先归一化再解析是跨区域配置解析的通用防御。
    • 验收样张要刻意复现失败场景(用四个高对比彩色画线),而不是用“本来就黑白”的图去验证黑白出图——后者会让回归测试永远通过。验证必须针对“修复前的真实失败行为”设计,否则测试通过毫无意义。
    • QPageSize::size() 是纸张尺寸,不含方向。离屏 QImage 当打印设备时,画布尺寸必须复用引擎的 effectivePageW/H() 换算,否则画布纵向、排版横向,静默裁掉

      87
       

      m

      m

      87\\,\\mathrm{mm}

      87mm。这类 bug 不报错只丢内容,且第一反应怀疑几何参数是本能误导——应先看输出图宽高比是否与设置方向一致。(QPdfWriter 路径自带方向处理,故只有离屏回归图会错。)

    • 回归测试的对照组必须精确对应“修复前的真实行为”。用“另一个也开了修复开关的配置”当对照组,会得到“无差异”的假通过。断言 PASS 之前,先确认它在修复前会 FAIL——这是回归测试设计的一条通用铁律。
    • “离屏出图工具”与“真实打印设备”之间存在环境差异(DPI、painter 生命周期、纸张方向),用 QImage 充当打印设备做回归时,必须显式对齐 DPI 与 painter 生命周期,否则会得到“全白”或“裁切”这类静默错误。

    9. 未来工作

  • CTB / STB 完整兼容:当前仅支持简化文本子集,可扩展至 AutoCAD 二进制 CTB 与 STB 命名样式表。二进制 CTB 解析需处理其专有的字节序与字段布局,STB 则需引入“命名样式”概念,二者可复用现有 PlotStyleEntry 作为统一内部表示,仅在导入/导出层做格式适配。
  • 按对象类型映射:新增第 4 匹配维度,支持“按图元类型”映射。当前三维匹配(图层 / 颜色 / 线型)已覆盖绝大多数场景,但“文字统一细线、剖面统一灰度”这类需求需要按图元类型(文字 / 剖面 / 标注 / 块引用)做映射,可扩展 byType_ 表并纳入优先级链。
  • 图案族扩展:HATCH 图案名从 ANSI31 扩展至 PAT 库。当前抖动仅覆盖 ANSI31 单一图案,扩展至完整 PAT 库后,剖面 / 填充可选用更丰富的纹理表达中间灰层次,需同步扩展 makeDitherBrush 的图案生成逻辑。
  • DPI 自适应:网点尺寸随 DPI 自适应,避免高 DPI 下网点过细。当前网点 tile 固定为 4 设备像素,在 300dpi 下纸面尺寸约

    0.34
     

    m

    m

    0.34\\,\\mathrm{mm}

    0.34mm,但在 600dpi 下会缩到

    0.17
     

    m

    m

    0.17\\,\\mathrm{mm}

    0.17mm,可能低于打印机的物理分辨率极限;自适应策略可按 pxPerMm 动态调整 tile 尺寸。

  • 色域映射:引入 CIELAB 色差度量,支持彩色

    →

    \\to

    → 灰度的感知一致映射。当前灰度化是简单的亮度线性映射,不同色相在感知上可能映射到相近灰度;引入 CIELAB

    Δ

    E

    \\Delta E

    ΔE 度量后,可让不同色相映射到感知均匀的灰度梯度,提升可辨识度。

  • ICC 配置文件:支持 ICC 配置文件,实现设备无关色彩管理。当前灰度化假设目标设备是理想灰阶设备,实际打印机存在色偏;引入 ICC 后,可在设备空间内做精确的灰平衡校正。
  • 预览缩略图:在页面设置对话框中提供样式表实时预览缩略图。当前用户需导入 CTB 后出样张才能看到效果,预览缩略图可让用户在提交打印前即时确认线宽分组与灰度意图,减少试打印成本。
  • 回归自动化:将双路回归纳入 CI,自动生成判定报告。当前双路回归由 PlotStyleSamples 手动触发,纳入 CI 后可在每次提交时自动跑 A4 / A3 双路比对,并把三指标判定结果写入构建产物,防止回归被后续改动悄悄破坏。
  • 多图幅批量预设:为 G1…G5 各图幅线组提供一键预设切换。当前 GB 预设从 LineWidthGroup 动态取档,已支持切换,但 UI 尚未提供“按图幅一键切换”入口;可新增预设下拉,让用户按出图图幅直接选择对应线组。
  • 灰度直方图可视化:在回归报告中附上两路灰度直方图叠加图。当前回归报告仅输出

    L

    1

    \\mathrm{L1}

    L1 数值,叠加直方图可让人工快速定位差异集中在哪个灰度区间,辅助判断差异是否合理(如集中在粗线档位)。

  • 9. 未来工作

  • CTB / STB 完整兼容:当前仅支持简化文本子集,可扩展至 AutoCAD 二进制 CTB 与 STB 命名样式表;
  • 按对象类型映射:新增第 4 匹配维度,支持“按图元类型”映射;
  • 图案族扩展:HATCH 图案名从 ANSI31 扩展至 PAT 库;
  • DPI 自适应:网点尺寸随 DPI 自适应,避免高 DPI 下网点过细;
  • 色域映射:引入 CIELAB 色差度量,支持彩色

    →

    \\to

    → 灰度的感知一致映射;

  • ICC 配置文件:支持 ICC 配置文件,实现设备无关色彩管理;
  • 预览缩略图:在页面设置对话框中提供样式表实时预览缩略图;
  • 回归自动化:将双路回归纳入 CI,自动生成判定报告。

  • 10. 结论

    本文给出了一套面向黑白出图的打印样式映射设计方案。该方案以“灰度与线宽正交”为核心命题,以三维匹配 + 三正交属性为数据结构、优先级短路为匹配语义、GB 预设为出厂配置、简化 CTB 为交换格式、抖动画刷为填充增强、双路回归为验证手段,系统解决了“彩色线宽图打印必错”问题。验证表明,A4 / A3 双路回归在三项指标(彩色像素、墨量、直方图

    L

    1

    \\mathrm{L1}

    L1)上均判定 PASS,样式表组彩色像素为 0,墨量提升

    +

    19.1

    %

    /

    +

    18.8

    %

    +19.1\\% / +18.8\\%

    +19.1%/+18.8%,线宽分组真实生效,黑白化彻底。


    附录 A:符号表

    符号含义

    C

    C

    C

    颜色维度

    W

    W

    W

    线宽维度

    B

    W

    \\mathrm{BW}

    BW

    黑白输出条件

    g

    g

    g

    目标灰度,

    g

    ∈

    [

    0

    ,

    1

    ]

    g \\in [0, 1]

    g∈[0,1]

    v

    v

    v

    输出 RGB 通道值

    α

    \\alpha

    α

    Alpha 通道

    r

    thick:thin

    r_{\\text{thick:thin}}

    rthick:thin​

    粗 : 细线宽比

    D

    P

    I

    \\mathrm{DPI}

    DPI

    每英寸点数

    p

    x

    P

    e

    r

    M

    m

    \\mathrm{pxPerMm}

    pxPerMm

    每毫米像素数

    ρ

    black

    \\rho_{\\text{black}}

    ρblack​

    黑点密度

    t

    tile

    t_{\\text{tile}}

    ttile​

    网点 tile 设备像素尺寸

    t

    paper

    t_{\\text{paper}}

    tpaper​

    网点 tile 纸面物理尺寸

    g

    ˉ

    \\bar{g}

    gˉ​

    平均灰度

    M

    M

    M

    总墨量

    L

    1

    \\mathrm{L1}

    L1

    灰度直方图

    L

    1

    \\mathrm{L1}

    L1 差异

    N

    color

    N_{\\text{color}}

    Ncolor​

    彩色像素计数

    附录 B:核心公式速查

    灰度化:

    v

    =

    c

    l

    a

    m

    p

    (

    r

    o

    u

    n

    d

    (

    g

    ×

    255

    )

    ,

     

    0

    ,

     

    255

    )

    ,

    R

    G

    B

    out

    =

    (

    v

    ,

    v

    ,

    v

    )

    v = \\mathrm{clamp}\\left(\\mathrm{round}(g \\times 255),\\ 0,\\ 255\\right), \\quad \\mathrm{RGB}_{\\text{out}} = (v, v, v)

    v=clamp(round(g×255), 0, 255),RGBout​=(v,v,v)

    粗细比:

    r

    thick:thin

    =

    w

    thick

    w

    thin

    =

    2

    r_{\\text{thick:thin}} = \\frac{w_{\\text{thick}}}{w_{\\text{thin}}} = 2

    rthick:thin​=wthin​wthick​​=2

    DPI 换算:

    p

    x

    P

    e

    r

    M

    m

    =

    D

    P

    I

    25.4

    \\mathrm{pxPerMm} = \\frac{\\mathrm{DPI}}{25.4}

    pxPerMm=25.4DPI​

    网点密度:

    ρ

    black

    =

    1

    −

    g

    \\rho_{\\text{black}} = 1 – g

    ρblack​=1−g

    网点纸面尺寸:

    t

    paper

    =

    t

    tile

    p

    x

    P

    e

    r

    M

    m

     

    m

    m

    t_{\\text{paper}} = \\frac{t_{\\text{tile}}}{\\mathrm{pxPerMm}}\\ \\mathrm{mm}

    tpaper​=pxPerMmttile​​ mm

    平均灰度:

    g

    ˉ

    =

    1

    N

    ∑

    i

    =

    1

    N

    g

    i

    \\bar{g} = \\frac{1}{N} \\sum_{i=1}^{N} g_i

    gˉ​=N1​i=1∑N​gi​

    总墨量:

    M

    =

    ∑

    i

    =

    1

    N

    (

    255

    −

    g

    i

    )

    M = \\sum_{i=1}^{N} (255 – g_i)

    M=i=1∑N​(255−gi​)

    直方图

    L

    1

    \\mathrm{L1}

    L1 差异:

    L

    1

    =

    ∑

    b

    =

    0

    255

    ∣

    H

    style

    (

    b

    )

    −

    H

    nostyle

    (

    b

    )

    ∣

    \\mathrm{L1} = \\sum_{b=0}^{255} \\left| H_{\\text{style}}(b) – H_{\\text{nostyle}}(b) \\right|

    L1=b=0∑255​∣Hstyle​(b)−Hnostyle​(b)∣

    彩色像素判据:

    N

    color

    =

    ∣

    {

    i

    ∣

    R

    i

    ≠

    G

    i

    ∨

    G

    i

    ≠

    B

    i

    }

    ∣

    =

    0

    N_{\\text{color}} = \\left| \\{ i \\mid R_i \\neq G_i \\lor G_i \\neq B_i \\} \\right| = 0

    Ncolor​=∣{i∣Ri​=Gi​∨Gi​=Bi​}∣=0

    优先级短路:

    resolve

    ⁡

    (

    e

    )

    =

    {

    E

    layer

    (

    e

    .

    l

    a

    y

    e

    r

    )

    ,

    若存在

    ,

    E

    color

    (

    e

    .

    c

    o

    l

    o

    r

    )

    ,

    若存在且无图层条目

    ,

    E

    style

    (

    e

    .

    s

    t

    y

    l

    e

    )

    ,

    若存在且无颜色条目

    ,

    default

    ,

    否则

    .

    \\operatorname{resolve}(e) = \\begin{cases} E_{\\text{layer}}(e.\\mathrm{layer}), & \\text{若存在}, \\\\ E_{\\text{color}}(e.\\mathrm{color}), & \\text{若存在且无图层条目}, \\\\ E_{\\text{style}}(e.\\mathrm{style}), & \\text{若存在且无颜色条目}, \\\\ \\text{default}, & \\text{否则}. \\end{cases}

    resolve(e)=⎩

    ⎨

    ⎧​Elayer​(e.layer),Ecolor​(e.color),Estyle​(e.style),default,​若存在,若存在且无图层条目,若存在且无颜色条目,否则.​


    附录 C:交付物清单

    类型文件
    核心样式 src/core/PlotStyle.{h,cpp}(灰度 / 抖动 / GB 预设 / CTB)
    打印引擎 src/ui/PrintEngine.{h,cpp}(monochrome + effectivePlotStyle)
    渲染器 src/ui/PlotRenderer.cpp(makeDitherBrush + 灰度画刷)
    UI 对话框 src/ui/PrintSettingsDialog.{h,cpp}(预设 / 导入 / 灰度控件)
    COM 批量 src/com/BatchPlotEngine.{h,cpp}(ctbText / preset 透传)
    样张工具 bench/PlotStyleSamples.cpp(A4 / A3 样张 + 双路回归)
    自测 t_plotStyle()(29 条断言)
    文档 本文
    赞(0)
    未经允许不得转载:171主机测评 » SketchCAD 打印样式映射:黑白出图的线宽分组与灰度正交设计
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址