矢量图像

在 0.50.0 版本加入.

终端图形协议 传输像素。绘图程序、文档查看器、绘图库和笔应用原本必须自行栅格化全部内容,窗口调整大小、视图缩放或新增笔划时重新发送像素;在慢速连接上,这使它们明显带有远程操作的迟滞感。矢量图像是图形协议的像素格式 f=200,传输数据不是像素,而是由路径、整形文本、字形轮廓、笔划、光栅图像和渐变组成的小型场景。终端保留场景,自行按实际显示尺寸栅格化,因此任意缩放级别都清晰,程序只需用几字节的补丁更新。该格式足以渲染 PDF 页面,见 渲染 PDF。

对协议其余部分而言,矢量图像仍是普通图像,其他机制均不变:a=p 放置、锚定字符单元以随文本滚入回滚缓冲区、备用屏幕、与文本的 z 层级、Unicode 占位符、相对放置、m=1 分块、o=z 压缩、全部传输介质、a=d 删除及 a=q 查询。存储图像的中继或多路复用器以相同方式存储矢量图像。

传输矢量图像

数据为 UTF-8 文本,每行一条 记录,使用 a=t 或 a=T 及 f=200 传输。s 和 v 必需,以记录所用坐标系的 用户单位 指定图像的 固有尺寸。显示方式与具有相应像素数的光栅图像一致:省略 c 和 r 时,占据 s 乘 v 像素;指定时,场景缩放到对应字符单元。例如,白页上的红色方块::

<ESC>_Ga=T,f=200,i=1,s=200,v=100,c=40,r=10;<base64 of the records below><ESC>\

p id=1 d=M0,0L200,0L200,100L0,100Z fill=#ffffff
p id=2 d=M20,20L80,20L80,80L20,80Z fill=#ff0000

与其他图像一样,以相同图像 ID 重新传输会替换整个场景。使用 o=z 时无须提供未压缩尺寸。成功回复 OK;场景解析失败则回复 EINVAL,附行号和问题描述,且不保留此次传输的任何内容。

编辑矢量图像

a=e 将载荷中的记录作为补丁,应用于 i 或 I 指定的已有矢量图像:相同 ID 替换已有元素,新 ID 添加元素,x 删除,a 追加几何数据。补丁要么完整应用,要么在任一记录错误时完全不应用,并回复 EINVAL。图像不存在时回复 ENOENT,目标为光栅图像时回复 EINVAL。分块和压缩与 a=t 相同::

<ESC>_Ga=e,i=1;<base64 of "g id=0 t=2,0,0,2,-100,-50">\ <ESC>\

这一条记录将整个场景以 100,50 为中心放大两倍,查看器即可如此平移和缩放页面:终端重新栅格化,无需重传。由于终端按放置尺寸栅格化,用户放大后,程序也不必额外操作来提高图像清晰度。

记录

每条记录以一个字符表示类型,后接单个空格分隔的 key=value 字段,不使用引号,值内不含空格。未知键和记录类型会被忽略,以便格式扩展。空行会被忽略。数值为十进制,可含小数和指数,数值列表以逗号分隔。

每个元素都具有由程序选择的 32 位无符号整数 id,在图像的全部记录类型间唯一。每个可绘制元素还具有所属分组 ID g,默认为始终存在的根分组 0。同一分组内的元素按创建顺序绘制,同级分组亦然。替换元素不改变其顺序。

g 分组

g id= [p=<parent group>] [t=<transform>] [clip=<path id>] [box=x,y,width,height] [o=<opacity>] [blend=normal|multiply|screen] [v=0|1]

分组通过 p 嵌套,携带 PDF 或 SVG 渲染器压入/弹出的状态:变换、裁剪、不透明度和混合模式。clip 指定 p 或 s 元素,以其几何形状和填充规则在分组坐标系中裁剪全部内容,并与外层分组的裁剪求交。o 为百分比,默认 100;较低值使分组作为整体合成,重叠子元素不会相互透出。blend 应用于组内每项绘制操作,multiply 可产生荧光笔效果。v=0 隐藏分组及其子树,而不删除。根分组接受除 p 外的全部属性,因此 g id=0 t=... 可平移和缩放整幅场景。

box 是该分组坐标系中的矩形裁剪。宽高必须非负,任一为零会裁掉全部内容。它与 clip 及全部祖先分组裁剪求交,见 嵌套窗口裁剪。替换分组时省略 box 会移除原矩形。

t 文本

t id= [g=] text=<base64 UTF-8 JSON> [t=<transform>]

将字符串或带样式文本段整形并保留为矢量轮廓。JSON 布局对象见 文本布局与测量。文本参与分组变换、裁剪、不透明度、混合及普通元素替换/删除。记录被接受时只生成一次轮廓,此后按放置分辨率栅格化。

p 路径

p id= [g=] d=<path> [fill=<color>|none] [rule=nz|eo] [stroke=<color>] [w=<width>] [cap=butt|round|square] [join=miter|round|bevel] [ml=<miter limit>] [dash=<lengths>] [dp=<dash phase>] [t=<transform>]

d 使用 SVG 路径语法,数值之间用逗号,不用空格:M、L、H、V、C、S、Q、T 和 Z,支持绝对形式及小写相对形式。后续数值超出一条命令所需数量时,重复该命令。不支持圆弧,应将其转换为曲线。既无 fill 也无 stroke 的路径默认黑色填充;只有 stroke 的路径不填充。rule 是填充规则,默认非零环绕。w、cap、join、ml``(默认 ``4)和 dash 与 SVG 含义相同;描边几何在元素自身坐标系中计算,因此非均匀变换会像拉伸路径一样拉伸描边。设置 vector_dash_limit 时,一条路径最多切分为相应数量的虚线:若某子路径使计数超过上限,则该子路径以及之后同样会超限的子路径均绘制为实线。默认 0 表示绘制全部虚线。长路径上的细密虚线可能消耗远超可见效果所需的计算。

s 符号

s id= d=<path>

不自行绘制的几何对象,通过 u 记录按需多次放置。一页文本可以只发送一次各字形轮廓作为符号,再为每个字形发送一个 u。

u 使用符号

u id= [g=] s=<symbol id> [t=<transform>] [fill=] [stroke=] [w=]

以给定样式绘制符号,默认黑色填充。

i 图像

i id= [g=] img=<image id> [t=<transform>] [o=<opacity>]

通过图像 ID 绘制先前由图形协议传输的光栅图像,按其像素尺寸放置,因此变换前一像素等于一个用户单位。采用双线性采样。图像不存在或自身为矢量图像时不绘制。

k 笔划

k id= [g=] pts=<x,y,width,...> [fill=<color>] [cap=round|butt]

墨迹:沿各点形成的带状几何,每点具有对应宽度,连接处及默认端点采用圆形。用于实时绘图,每个笔事件通过一条 a 记录追加。终端也可随笔移动自行应用这些记录,见 终端侧墨迹。

a 追加

a id= d=<path> 或 a id= pts=<x,y,width,...>

向路径或符号追加几何(d),或向笔划追加点(pts)。追加路径数据独立解析,因此应以绝对坐标命令开始。

l 和 r 渐变

l id= x1= y1= x2= y2= stops=<offset,color,offset,color,...>

r id= cx= cy= r= [fx= fy=] stops=<...>

沿直线的线性渐变,或带可选焦点的径向渐变,使用该渐变的元素坐标系。色标偏移从 0 到 1。通过 fill=@<id> 或 stroke=@<id> 将渐变作为颜色使用。

x 删除

x id=<element>、x g=<group> 或 x all

删除一个元素、分组及其全部内容,或整个场景。删除被其他元素引用的符号、渐变或裁剪时,引用元素仍存在,但失去对应资源。

颜色可用 sRGB #rrggbb、#rrggbbaa,或 none。浮点形式 srgb:r/g/b[/a]、p3:r/g/b[/a] 和 rec2020:r/g/b[/a] 使用 0 至 1 的分量、指定基色和 sRGB 传递曲线,与 广色域颜色 一致。linear:r/g/b[/a] 为线性扩展 sRGB:RGB 可为负或超过 1,Alpha 范围 0 至 1,默认 1。所有数值必须有限。斜杠分隔可避免与逗号分隔的渐变色标混淆。

各种颜色形式均接受 *<percent>,按该百分比乘线性 RGB 而不改变 Alpha,例如 #ff8800*250 或 p3:1/0.2/0*300。适用于填充、描边、笔划、符号、文本及装饰和渐变色标。渐变引用 @<id> 也可用于文本绘制色。合成和渐变插值在线性扩展 sRGB 中进行,嵌套分组及裁剪保留 HDR 和超出 sRGB 的通道。嵌入光栅图像保留标注色域及 f=64 HDR 值。GPU 纹理使用 RGBA16F(有限通道值饱和到其可表示范围 ±65504),呈现时转换到显示色域及亮度余量。图像上的 T=1 选择 HDR 亮度协议 色调映射。SDR 输出使用终端已有的 HDR 转 SDR 呈现路径。

变换为六值仿射矩阵 t=a,b,c,d,e,f,将 x, y 映射为 a*x + c*y + e, b*x + d*y + f,遵循 SVG 和 PDF 约定。

嵌套窗口裁剪

裁剪在所属分组变换之后,以设备空间蒙版保留。后代的平移、旋转、缩放或逆变换不能越出外层裁剪。嵌套没有固定上限;可选 vector_depth_limit 与其他嵌套分组相同。对于 odin-seat 等窗口管理器,把各窗口内容放在带外层裁剪的分组下,再嵌套内容和控件裁剪::

g id=1000 t=1,0,0,1,50,30 box=0,0,400,240
g id=1001 p=1000 box=10,40,380,190
g id=1002 p=1001 t=0.866,0.5,-0.5,0.866,0,0 box=0,0,200,100
p id=1003 g=1002 d=M-100,-100L1000,-100L1000,1000L-100,1000Z fill=#336699*200

蓝色图形始终位于全部三个矩形内。移动外层分组时,其裁剪和内容一起移动。窗口管理器合并场景时分配元素 ID,将子场景挂在窗口分组下;box 约束该子树,不影响其他根级元素。

矢量协议中的字体资源

a=v,f=200,i=<request id> 是矢量服务。载荷为 UTF-8 JSON,使用普通图形协议的 base64 编码、分块、压缩及直接/文件/临时文件/共享内存传输。不需要 s 或 v,也不创建图像或放置。i 用于关联回复,不占用图像 ID。后续块可省略操作,沿用首块参数。使用普通 kitty 图形块,每块最多 4096 字节 base64。例如::

<ESC>_Ga=v,f=200,i=77;<base64 of {"op":"fonts"}><ESC>\
<ESC>_Gi=77;OK;<base64 UTF-8 JSON result><ESC>\

错误为 EINVAL:<base64 of {"error":"description"}>;传输错误使用普通图形错误回复。q 沿用静默语义;需要结果时使用 q=0。成功结果正文随数据增长,不截断为普通的简短图形消息。

{"op":"fonts"}

列出已安装字体(字体族、样式、PostScript 名称、字重、原生倾斜角度和是否等宽)、配置角色及已上传的会话字体 ID。version:1 标识本套文本/资源协议。字体族和 PostScript 名称可作为 font 选择器。

{"op":"inspect","font":"Iosevka"}

报告所选字体每 em 单位数、字形数、字重、原生倾斜,以及可变轴的标签、最小值、默认值和最大值。也接受角色、字重和意大利体选择。

{"op":"upload","id":"body","data":"<base64 font bytes>","index":0}

将 TrueType/OpenType 轮廓字体加载到内存。index 选择字体集合成员,默认零。非空字符串 ID 使用独立于图像和元素 ID 的命名空间。复用 ID 会在验证成功后原子替换字体。回复包含 ID 和检查元数据。使用 "font":"@body" 选择它。字体字节不会写入图像磁盘缓存,也不会注册到操作系统。

{"op":"release","id":"body"}

释放会话句柄。已保留文本仍持有其轮廓;之后使用该句柄的布局会失败,直到重新上传。上传资源局限于当前 PTY,在主缓冲区和备用缓冲区间共享,会话关闭时释放。删除图像不释放字体。字体数量没有固定限制,应用应在用完后释放资源。传输分配错误会被报告。

文本布局与测量

文本记录解码后的 JSON 是布局对象,例如::

{"font":"Iosevka","size":24,"x":10,"y":30,"angle":-12,
 "runs":[
   {"text":"Window ","fill":"#dddddd"},
   {"text":"title","weight":700,"italic":true,"slant":0,
    "underline":"double","fill":"p3:1/0.2/0*250"}
 ]}

单字符串使用 text,多段使用 runs 数组,其中每个对象包含 text 字符串及样式覆盖。顶层样式作为各段默认值,各段沿同一基线连续排布。应按预期的文本整形边界拆分:样式变化也会终止连字或文字整形段。JSON 转义可保留空格、换行、Unicode 和控制字符,而不引入额外场景记录。

font / role

选择已安装字体族/PostScript 名称或 @ 会话句柄,或使用 字体选择协议 中配置的 sans、serif 等角色。默认使用终端配置的常规字体(Iosevka)。矢量文本也接受比例轮廓字体。

size / weight / italic / slant

以场景单位表示的正 em 尺寸(默认 16)、字重 100–900(默认 400)、意大利体字形(布尔值,默认 false),以及可选的绝对倾斜角度 -15 至 15 度。负值向右倾;零可使意大利体字形直立;正值合成反向倾斜。已安装静态字体族选择最接近的字体;可变字体使用字重轴。上传的静态字体保留原生字重和字形,应分别上传所需字体,或上传可变字体。

axes / features

分别为受支持的四字符 OpenType 轴标签到设计值的字典,以及 HarfBuzz 特性字符串数组,例如 ["liga=0", "ss01=1"]。明确指定的轴值必须位于字体范围内。

direction / language / script / fallback

可选的整形方向(ltr、rtl、ttb 或 btt)、BCP47 语言和 ISO15924 文字。未指定属性由 HarfBuzz 推断。可用字体选择器数组提供回退字体;缺失字形簇时也会尝试所配置的地区中日韩字体。选择回退时,组合附加符和连接序列保持完整。这是文本段布局 API:混合双向段落的视觉段序由应用提供,换行由应用指定,不自动折行。纯位图字形和内嵌彩色字形绘制不属于轮廓文本格式。

fill / stroke / stroke_width / background

矢量绘制色,支持渐变、HDR 和广色域。填充默认黑色,描边默认 none,描边宽度默认 1,文本描边使用圆角连接。背景可选,填充各文本段的逻辑矩形。

underline / underline_color / strike / overline

下划线样式为 none``(默认)、``single、double、curly、dotted 或 dashed;可选绘制色默认与填充相同。删除线和上划线为布尔值。装饰由矢量几何构成,接受与字形相同的 HDR/广色域绘制色和变换。

x / y / angle

顶层基线原点(默认 0,0)及旋转角度(默认 0 度)。场景 y 轴向下,因此正角度为顺时针。\n 开始新行。随后应用记录的仿射变换及全部父级变换。可用分组矩形将文本裁剪到视口。

通过 a=v 发送同样的布局对象,并设置 "op":"measure",即可测量而不创建图像或栅格化像素。结果包含未旋转逻辑布局的 width、height``(用户单位),应用原点/旋转前的 ``advance:[x,y],ascent、descent,以及应用布局原点/旋转后的 ink:[left,top,right,bottom];没有绘制几何时为 null。墨迹范围包括装饰、背景和描边延伸,曲线极值从整形轮廓测得。missing 列出未解析字形的 Unicode 标量索引,按所有文本段连接后的顺序计数。渲染使用相同布局代码。测量先于场景变换、渐变查询、不透明度和裁剪;即使窗口只显示被裁剪的一部分,也报告完整字符串。

Python 客户端辅助函数

kittens.tui.vector 提供 service_request、font_upload、measure_text、parse_service_response、text_record、clip_group 和 scene。传输辅助函数为应用输出队列生成正确分块的图形序列。回复解析器接受完整 APC 帧,或输入解析器给出的 G 前缀正文,返回请求 ID 和解码结果。错误回复会抛出 ValueError。例如,上传 body 并收到成功回复后::

from kittens.tui.vector import clip_group, measure_text, scene, text_record

layout = {"font": "@body", "size": 24, "text": "Window title",
          "x": 8, "y": 28, "fill": "p3:1/0.3/0*200"}
for frame in measure_text(77, layout):
    output_queue.write(frame)
# Handle request 77's result in the application's normal input loop.
records = [clip_group(100, [0, 0, 400, 240]),
           clip_group(101, [4, 4, 392, 32], parent=100),
           text_record(102, layout, group=101)]
for frame in scene(1, 400, 240, records):
    output_queue.write(frame)

渲染

场景首次显示、场景变化或显示尺寸改变时,终端使用抗锯齿进行栅格化,并按固有尺寸填满放置区域的方式缩放用户单位;灰度和彩色显示器上的形状外观一致。场景按元素大小计入图像存储配额,而非按像素计数。

Silkie 在 CPU 上栅格化。在高像素密度的大窗口中绘制整个场景可能需要几十到几百毫秒,这是首次显示、调整尺寸及部分补丁的成本。多数补丁更便宜,因为 kitty 会计算可能受影响的矩形,仅重绘该区域,结果与全量绘制逐像素一致:

  • 向笔划,或带描边且 fill=none 的路径追加内容时,只绘制新增几何周围的矩形,而非整条笔划的矩形。这就是实时墨迹的绘制成本。

  • 新增、替换或删除路径、文本、符号使用实例或笔划时,重绘元素旧位置和新位置周围的矩形。向填充路径追加内容同样如此,因为填充会跨越子路径两端间的缺口闭合,受影响范围不止新增部分。

  • g 记录(包括根分组)、符号、渐变或图像记录,以及删除它们、x g=、x all,或改变作为分组裁剪的路径,都会全量重绘,因为其他元素依赖这些资源。通过根分组变换进行平移或缩放也属于此类。

因此动画程序应把变化限制在独立的小元素中。除用于显示的纹理外,kitty 不保留其他栅格化场景副本,所以只有元素本身计入配额。

渲染 PDF

记录与 PDF 内容流操作符足够接近,查看器可以不自行栅格化而渲染页面:

  • q 和 Q 分别对应 g 记录及其结束,cm 对应 t。

  • W n 对应包含裁剪路径的 p 记录,以及包围后续内容分组上的 clip=。

  • 路径构建和绘制操作符转换为 p 记录,奇偶填充使用 rule=eo;线宽、端点、连接、斜接限制和虚线等图形状态参数转换为 w、cap、join、ml 和 dash。

  • 文本:每个使用的字形从内嵌字体提取轮廓,作为 s 记录只发送一次;各 Tj 转换为 u 记录,以文本矩阵作为 t。需要描边的文本渲染模式使用 stroke。

  • 图像 XObject 的 Do 先通过图形协议传输图像一次,再用 i 绘制。PDF 的单位正方形图像空间,对宽 w、高 h 像素的图像,对应变换 t=w,0,0,-h,0,h,随后再应用当前矩阵。

  • ca 和 CA 对应分组的 o;BM 的 normal、multiply、screen 模式对应 blend;轴向和径向着色分别对应 l、r 渐变。

本版本不支持软蒙版、作为填充的平铺和着色图案、挖空与隔离分组,以及其他混合模式;查看器应自行栅格化这些部分,再作为光栅图像发送。

中继与多路复用器

像 mosh 那样为客户端重建屏幕的中继,以 ID 为键将矢量图像保存为记录表。新 ID 添加,相同 ID 替换,x 删除(分组 x g= 同时删除父级链通向它的全部元素),a 拼接到目标几何,a=t 或 a=T 则先清空记录表再添加。将表写成记录即得到完整图像状态,可在重连后以一次 a=t 传给新终端,无需保存补丁历史。会话字体也必须在引用它们的文本记录之前重放上传。不理解记录的中继仍可把它们作为不透明图像数据按顺序转发,保留传输与补丁的先后关系。

查询支持情况

查询终端是否支持矢量图像,可发送 f=200 和一个小型场景::

<ESC>_Ga=q,i=31,f=200,s=1,v=1;cCBpZD0xIGQ9TTAsMEwxLDBMMSwxWg==<ESC>\

支持的终端回复 OK,不支持的回复 EINVAL 或不回答;后者使用通常的超时处理。

该查询仅确认基础场景支持。若要使用整形文本、字体资源、测量、矩形裁剪和 HDR 绘制色,请以 a=v,f=200 查询 {"op":"fonts"},检查 version 和 features。较早的矢量实现可能接受基础场景,却不具备这些扩展。