利用Python实现矢量路径的压缩、解压与可视化

打印 上一主题 下一主题

主题 1374|帖子 1374|积分 4122

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
弁言

在图形计划和Web开发中,矢量路径数据的高效存储与传输至关重要。本文将通过一个Python示例,展示如何将复杂的矢量路径命令序列压缩为JSON格式,再将其解压还原,并通过matplotlib进行可视化。这一过程可应用于字体计划、矢量图形编辑或Web应用中的路径数据传输。

焦点功能概述

1. 路径命令解析



  • 输入:包罗moveTo、lineTo、qCurveTo(二次贝塞尔曲线)、closePath命令的路径数据。
  • 输出:转换为matplotlib.path.Path对象,用于绘制矢量图形。
2. 路径数据压缩



  • 将路径命令序列转换为紧凑的JSON格式,便于存储或传输。
  • 示例:moveTo((100, 177)) → {"M":[100,177]}。
3. 路径数据解压



  • 将JSON格式还原为原始路径命令序列,确保数据完整性。
4. 可视化



  • 利用matplotlib渲染路径,验证压缩/解压过程的正确性。

代码实现详解

1. 路径命令解析(parse_commands函数)

  1. def parse_commands(data):
  2.     codes = []
  3.     vertices = []
  4.     for cmd, params in data:
  5.         if cmd == 'moveTo':
  6.             codes.append(Path.MOVETO)
  7.             vertices.append(params[0])
  8.         elif cmd == 'lineTo':
  9.             codes.append(Path.LINETO)
  10.             vertices.append(params[0])
  11.         elif cmd == 'qCurveTo':
  12.             # 处理二次贝塞尔曲线(每段需要两个控制点和一个终点)
  13.             for i in range(0, len(params), 2):
  14.                 control = params[i]
  15.                 end = params[i+1] if i+1 < len(params) else params[-1]
  16.                 codes.extend([Path.CURVE3, Path.CURVE3])
  17.                 vertices.extend([control, end])
  18.         elif cmd == 'closePath':
  19.             codes.append(Path.CLOSEPOLY)
  20.             vertices.append(vertices[0])  # 闭合路径回到起点
  21.     return codes, vertices
复制代码
关键点:



  • 二次贝塞尔曲线:qCurveTo命令需两个控制点和一个终点,通过Path.CURVE3实现。
  • 闭合路径:CLOSEPOLY命令自动连接末了一个点到出发点。

2. 路径数据压缩(compress_path_to_json函数)

  1. def compress_path_to_json(data):
  2.     command_map = {'moveTo': 'M', 'lineTo': 'L', 'qCurveTo': 'Q', 'closePath': 'Z'}
  3.     compressed = []
  4.     for cmd, params in data:
  5.         cmd_short = command_map[cmd]
  6.         points = []
  7.         if cmd == 'closePath':
  8.             compressed.append({cmd_short: []})
  9.         else:
  10.             # 将坐标元组展平为一维列表(如 [(x,y), (a,b)] → [x,y,a,b])
  11.             for coord in params:
  12.                 points.extend(list(coord))
  13.             compressed.append({cmd_short: points})
  14.     return json.dumps(compressed, separators=(',', ':'))
复制代码
示例输出:

  1. [{"M":[100,177]},{"L":[107,169]},{"Q":[116,172,127,172]},...]
复制代码

3. 路径数据解压(decompress_json_to_path函数)

  1. def decompress_json_to_path(compressed_json):
  2.     command_map = {'M': 'moveTo', 'L': 'lineTo', 'Q': 'qCurveTo', 'Z': 'closePath'}
  3.     data = json.loads(compressed_json)
  4.     decompressed = []
  5.     for item in data:
  6.         cmd_short = next(iter(item))
  7.         points = item[cmd_short]
  8.         cmd = command_map[cmd_short]
  9.         if not points:
  10.             decompressed.append((cmd, ()))  # 闭合路径无参数
  11.         else:
  12.             # 将一维列表转换为坐标元组(如 [x,y,a,b] → [(x,y), (a,b)])
  13.             coords = []
  14.             for i in range(0, len(points), 2):
  15.                 coords.append((points[i], points[i+1]))
  16.             decompressed.append((cmd, tuple(coords)))
  17.     return decompressed
复制代码

4. 可视化渲染(show_ttf函数)

  1. def show_ttf(data):
  2.     codes, vertices = parse_commands(data)
  3.     path = Path(vertices, codes)
  4.     fig, ax = plt.subplots()
  5.     patch = patches.PathPatch(path, facecolor='orange', lw=2)
  6.     ax.add_patch(patch)
  7.     ax.set_xlim(0, 250)  # 根据数据范围调整坐标轴
  8.     ax.set_ylim(-30, 220)
  9.     plt.gca().set_aspect('equal')
  10.     plt.show()
复制代码

完整代码与运行结果

示例数据

  1. data = [
  2.     ('moveTo', ((100, 177),)),
  3.     ('lineTo', ((107, 169),)),
  4.     ('qCurveTo', ((116, 172), (127, 172))),
  5.     # ... 其他路径命令(如闭合路径、复杂曲线)
  6. ]
复制代码
执行流程

  1. # 压缩数据
  2. compressed_json = compress_path_to_json(data)
  3. print("压缩后的JSON:", compressed_json)
  4. # 解压数据
  5. decompressed = decompress_json_to_path(compressed_json)
  6. print("解压后的路径数据:", decompressed)
  7. # 可视化
  8. show_ttf(decompressed)
复制代码

结果展示

1. 压缩后的JSON片段

  1. [
  2.   {"M":[100,177]},
  3.   {"L":[107,169]},
  4.   {"Q":[116,172,127,172]},
  5.   {"Z":[]}
  6. ]
复制代码
2. 解压后的路径数据

  1. [
  2.     ('moveTo', ((100, 177),)),
  3.     ('lineTo', ((107, 169),)),
  4.     ('qCurveTo', ((116, 172), (127, 172))),
  5.     ('closePath', ())
  6. ]
复制代码
技术要点总结


  • 路径命令映射

    • M → moveTo:移动到出发点
    • L → lineTo:绘制直线
    • Q → qCurveTo:二次贝塞尔曲线
    • Z → closePath:闭合路径

  • JSON压缩策略

    • 将坐标元组展平为一维列表,减少冗余。
    • 闭合路径(Z)的参数为空列表。

  • matplotlib路径渲染

    • 利用Path对象和PathPatch实现复杂曲线的绘制。
    • CURVE3命令需成对利用,适配二次贝塞尔曲线的参数。


应用场景



  • Web开发:将矢量路径数据嵌入SVG或Canvas元素。
  • 字体计划:存储和传输字体表面路径。
  • 数据可视化:动态天生并传输图表路径数据。

  1. import matplotlib.pyplot as plt
  2. from matplotlib.path import Path
  3. import matplotlib.patches as patches
  4. # 解析输入数据
  5. def parse_commands(data):
  6.     codes = []
  7.     vertices = []
  8.     for command, params in data:
  9.         if command == 'moveTo':
  10.             codes.append(Path.MOVETO)
  11.             vertices.append(params[0])
  12.         elif command == 'lineTo':
  13.             codes.append(Path.LINETO)
  14.             vertices.append(params[0])
  15.         elif command == 'qCurveTo':
  16.             # Check if there are enough points to form a quadratic Bezier curve segment
  17.             for i in range(0, len(params) - 1, 2):  # Ensure we don't go out of bounds
  18.                 control_point = params[i]
  19.                 end_point = params[i + 1]
  20.                 codes.extend([Path.CURVE3, Path.CURVE3])  # Two CURVE3 commands for the quad Bezier
  21.                 vertices.extend([control_point, end_point])
  22.         elif command == 'closePath':
  23.             codes.append(Path.CLOSEPOLY)
  24.             vertices.append(vertices[0])  # Closing back to the start point
  25.     return codes, vertices
  26. def show_ttf():
  27.     codes, vertices = parse_commands(data)
  28.     path = Path(vertices, codes)
  29.     fig, ax = plt.subplots()
  30.     patch = patches.PathPatch(path, facecolor='orange', lw=2)
  31.     ax.add_patch(patch)
  32.     ax.set_xlim(0, 250)  # Adjust these limits based on your data's extent
  33.     ax.set_ylim(-30, 220)  # Adjust these limits based on your data's extent
  34.     plt.gca().set_aspect('equal', adjustable='box')  # Keep aspect ratio equal
  35.     plt.show()
  36. import json
  37. def compress_path_to_json(data):
  38.     command_map = {
  39.         'moveTo': 'M',
  40.         'lineTo': 'L',
  41.         'qCurveTo': 'Q',
  42.         'closePath': 'Z'
  43.     }
  44.     compressed = []
  45.     for cmd, params in data:
  46.         command_type = command_map[cmd]
  47.         points = []
  48.         if cmd == 'closePath':
  49.             pass  # closePath无需坐标
  50.         else:
  51.             # 确保params[0]是坐标点列表(即使只有一个点)
  52.             for param in params:
  53.                 points += list(param)
  54.         compressed.append({
  55.             command_type: points
  56.         })
  57.     return json.dumps(compressed, separators=(',', ':'))
  58. data = [('moveTo', ((100, 177),)), ('lineTo', ((107, 169),)), ('qCurveTo', ((116, 172), (127, 172))),
  59.         ('lineTo', ((240, 172),)), ('lineTo', ((224, 190),)), ('lineTo', ((212, 177),)), ('lineTo', ((175, 177),)),
  60.         ('qCurveTo', ((183, 186), (176, 200), (154, 210))), ('lineTo', ((152, 207),)),
  61.         ('qCurveTo', ((164, 190), (166, 177))), ('closePath', ()), ('moveTo', ((204, 143),)), ('lineTo', ((211, 148),)),
  62.         ('lineTo', ((198, 162),)), ('lineTo', ((189, 152),)), ('lineTo', ((143, 152),)), ('lineTo', ((128, 160),)),
  63.         ('qCurveTo', ((129, 149), (129, 116), (128, 102))), ('lineTo', ((142, 106),)), ('lineTo', ((142, 114),)),
  64.         ('lineTo', ((191, 114),)), ('lineTo', ((191, 105),)), ('lineTo', ((205, 111),)),
  65.         ('qCurveTo', ((204, 119), (204, 135), (204, 143))), ('closePath', ()), ('moveTo', ((142, 147),)),
  66.         ('lineTo', ((191, 147),)), ('lineTo', ((191, 119),)), ('lineTo', ((142, 119),)), ('closePath', ()),
  67.         ('moveTo', ((119, 87),)), ('lineTo', ((218, 87),)), ('lineTo', ((218, 6),)),
  68.         ('qCurveTo', ((218, -3), (210, -5), (181, -3))), ('lineTo', ((181, -8),)),
  69.         ('qCurveTo', ((212, -13), (212, -26))), ('qCurveTo', ((221, -22), (231, -12), (231, 2))),
  70.         ('lineTo', ((231, 80),)), ('lineTo', ((240, 87),)), ('lineTo', ((224, 102),)), ('lineTo', ((216, 92),)),
  71.         ('lineTo', ((119, 92),)), ('lineTo', ((105, 100),)), ('qCurveTo', ((106, 84), (106, 5), (105, -26))),
  72.         ('lineTo', ((119, -18),)), ('closePath', ()), ('moveTo', ((196, 58),)), ('lineTo', ((203, 63),)),
  73.         ('lineTo', ((188, 76),)), ('lineTo', ((182, 67),)), ('lineTo', ((151, 67),)), ('lineTo', ((137, 76),)),
  74.         ('qCurveTo', ((138, 59), (138, 30), (137, 5))), ('lineTo', ((150, 11),)), ('lineTo', ((150, 21),)),
  75.         ('lineTo', ((184, 21),)), ('lineTo', ((184, 10),)), ('lineTo', ((197, 16),)),
  76.         ('qCurveTo', ((196, 27), (196, 48), (196, 58))), ('closePath', ()), ('moveTo', ((150, 62),)),
  77.         ('lineTo', ((184, 62),)), ('lineTo', ((184, 26),)), ('lineTo', ((150, 26),)), ('closePath', ()),
  78.         ('moveTo', ((36, 63),)), ('qCurveTo', ((66, 100), (94, 148))), ('lineTo', ((103, 152),)),
  79.         ('lineTo', ((83, 163),)), ('qCurveTo', ((74, 138), (66, 125))), ('lineTo', ((30, 123),)),
  80.         ('qCurveTo', ((50, 154), (71, 193))), ('lineTo', ((82, 197),)), ('lineTo', ((59, 209),)),
  81.         ('qCurveTo', ((51, 178), (23, 124), (14, 124))), ('lineTo', ((25, 106),)),
  82.         ('qCurveTo', ((31, 111), (50, 117), (63, 119))), ('qCurveTo', ((44, 87), (24, 63), (18, 62))),
  83.         ('lineTo', ((28, 44),)), ('qCurveTo', ((39, 51), (68, 60), (98, 66))), ('lineTo', ((97, 70),)),
  84.         ('qCurveTo', ((67, 66), (36, 63))), ('closePath', ()), ('moveTo', ((11, 14),)), ('lineTo', ((21, -4),)),
  85.         ('qCurveTo', ((30, 4), (65, 20), (95, 30))), ('lineTo', ((94, 34),)),
  86.         ('qCurveTo', ((72, 28), (25, 16), (11, 14))), ('closePath', ())]
  87. def decompress_json_to_path(compressed_json):
  88.     command_map = {
  89.         'M': 'moveTo',
  90.         'L': 'lineTo',
  91.         'Q': 'qCurveTo',
  92.         'Z': 'closePath'
  93.     }
  94.     data = json.loads(compressed_json)
  95.     decompressed = []
  96.     for item in data:
  97.         cmd_char = next(iter(item))  # 获取命令字符
  98.         points = item[cmd_char]
  99.         original_cmd = command_map[cmd_char]
  100.         if not points:
  101.             # closePath,参数为空
  102.             decompressed.append((original_cmd, ()))
  103.         else:
  104.             # 将points列表转换为坐标点元组的元组
  105.             tuples = []
  106.             for i in range(0, len(points), 2):
  107.                 x = points[i]
  108.                 y = points[i + 1]
  109.                 tuples.append((x, y))
  110.             params = tuple(tuples)
  111.             decompressed.append((original_cmd, params))
  112.     return decompressed
  113. compressed_json = compress_path_to_json(data)
  114. # 解压
  115. decompressed = decompress_json_to_path(compressed_json)
复制代码
免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。
回复

使用道具 举报

0 个回复

倒序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

李优秀

论坛元老
这个人很懒什么都没写!
快速回复 返回顶部 返回列表