Drawbot:让我们黑掉一些可爱的东西 (2025)
Drawbot: Let's hack something cute (2025)

原始链接: https://www.atredis.com/blog/2025/9/30/drawbot-lets-hack-something-cute

这个Python脚本从二进制数据源(如闪存转储)中提取并保存图形为SVG文件。它将二进制数据解析为指定大小的“槽”,然后将每个槽解释为定义图形的一系列笔画。 该脚本为每个图形计算边界框,并生成表示笔画的SVG文件。它提供以下选项: * 指定输入二进制文件、输出目录、槽起始偏移量和槽大小。 * 根据文件大小自动检测槽的数量。 * 过滤掉点太少的槽。 * 控制SVG输出中是否翻转Y轴。 最后,它报告成功导出的SVG数量,以及用于提取的参数。核心函数`write_svg`处理SVG文件内容的创建。

Hacker News上有一篇帖子讨论了“Drawbot”,这是一种面向儿童的小型笔式绘图仪(atredis.com)。最初的帖子和后续评论主要集中在该设备的简单性和扩展潜力上。 用户推测未来的迭代版本,建议的功能包括与大型语言模型连接的语音识别以生成绘画,或集成“turtle graphics”系统来编程绘画。一位评论者赞赏了对该设备的详细拆解。 值得注意的是,由于Hacker News常规 repost 系统出现技术问题,该帖子是重新发布,用户“dang”对此进行了确认。 讨论强调了对易于使用的创作工具的兴趣,以及人工智能在物理教育设备中的整合潜力。
相关文章

原文
def write_svg(strokes, out: Path, flip_y=True, stroke_w=2):
    bb = bbox(strokes)
    if not bb:
        return False
    xmin, ymin, xmax, ymax = bb
    w, h = xmax - xmin + 1, ymax - ymin + 1
    with out.open("w", encoding="utf-8") as f:
        if flip_y:
            f.write(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{xmin} {ymin} {w} {h}" '
                    f'width="{w}" height="{h}" stroke="black" fill="none" stroke-width="{stroke_w}">\n')
            f.write(f'  <g transform="translate(0,{ymin + ymax}) scale(1,-1)">\n')
            for s in strokes:
                f.write('    <path d="M{},{} {}"/>\n'.format(
                    s[0][0], s[0][1], " ".join(f"L{x},{y}" for x, y in s[1:])))
            f.write('  </g>\n</svg>\n')
        else:
            f.write(f'<svg xmlns="http://www.w3.org/2000/svg" viewBox="{xmin} {ymin} {w} {h}" '
                    f'width="{w}" height="{h}" stroke="black" fill="none" stroke-width="{stroke_w}">\n')
            for s in strokes:
                f.write('  <path d="M{},{} {}"/>\n'.format(
                    s[0][0], s[0][1], " ".join(f"L{x},{y}" for x, y in s[1:])))
            f.write('</svg>\n')
    return True

def main():
    ap = argparse.ArgumentParser(description="Extract SVGs from fixed slots")
    ap.add_argument("bin", type=Path, help="full flash dump (e.g., full.bin)")
    ap.add_argument("--out", type=Path, default=Path("svgs"), help="output directory")
    ap.add_argument("--base", type=lambda x:int(x,0), default=0x04, help="first slot start offset (default 0x04)")
    ap.add_argument("--slot", type=lambda x:int(x,0), default=0xEA60, help="slot size (default 0xEA60)")
    ap.add_argument("--count", type=int, default=None, help="number of slots (default: autodetect from file size)")
    ap.add_argument("--min-pts", type=int, default=2, help="min points to export (default 2)")
    ap.add_argument("--no-flip-y", action="store_true", help="do not flip Y axis")
    args = ap.parse_args()

    data = args.bin.read_bytes()
    size = len(data)

    if args.count is None:
        # Best-effort autodetect
        usable = max(0, size - args.base)
        args.count = usable // args.slot

    args.out.mkdir(parents=True, exist_ok=True)
    exported = 0

    for i in range(args.count):
        off = args.base + i * args.slot
        if off >= size:
            break
        slot = data[off : min(off + args.slot, size)]
        words = u16be_words(slot)
        strokes = strokes_from_slot(words)
        if sum(len(s) for s in strokes) < args.min_pts:
            continue
        svg_path = args.out / f"drawing_{i:03d}.svg"
        if write_svg(strokes, svg_path, flip_y=not args.no_flip_y, stroke_w=2):
            exported += 1
            # uncomment for debug:
            # print(f"{i:03d} @ 0x{off:08X} -> {svg_path.name}")
    print(f"Exported {exported} SVGs to {args.out} from {args.count} slots "
          f"(base=0x{args.base:X}, slot=0x{args.slot:X}).")
联系我们 contact @ memedata.com