figma-to-html-pixel-perfect 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,93 @@
1
+ #!/usr/bin/env python3
2
+ """Print an accurate layout spec for a Figma node, from cached node JSON.
3
+
4
+ Implements the §6.5.3 reading contract:
5
+ - text content comes from `characters`, never `name`
6
+ - `visible: false` subtrees are skipped
7
+ - reports textCase / textAlign / colour / stroke / radius / layout / image refs
8
+
9
+ Usage:
10
+ python3 figma_spec.py <path-to-node.json> [max_depth]
11
+
12
+ Where <path-to-node.json> is one entry of GET /v1/files/:key/nodes?ids=…
13
+ (i.e. a dict with a "document" key), as written by figma_pull.py.
14
+ """
15
+ import json, sys
16
+
17
+ if len(sys.argv) < 2:
18
+ sys.exit(__doc__)
19
+
20
+ path = sys.argv[1]
21
+ maxd = int(sys.argv[2]) if len(sys.argv) > 2 else 6
22
+
23
+ doc = json.load(open(path))
24
+ doc = doc["document"] if "document" in doc else doc
25
+ root = doc.get("absoluteBoundingBox") or {}
26
+ ox, oy = root.get("x", 0), root.get("y", 0)
27
+ print(f"### {path} {int(root.get('width',0))}x{int(root.get('height',0))}\n")
28
+
29
+
30
+ def _col(c, opacity=1):
31
+ a = opacity * c.get("a", 1)
32
+ r, g, b = (round(c[k] * 255) for k in "rgb")
33
+ return f"#{r:02x}{g:02x}{b:02x}" + (f"@{a:.2f}" if a < 0.99 else "")
34
+
35
+
36
+ def paint(node, key="fills"):
37
+ for f in node.get(key) or []:
38
+ if f.get("visible") is False:
39
+ continue
40
+ t = f.get("type")
41
+ if t == "SOLID":
42
+ return _col(f["color"], f.get("opacity", 1))
43
+ if t == "IMAGE":
44
+ # imageTransform may rotate/flip the photo — flag it, do not ignore it.
45
+ flag = "*" if f.get("imageTransform") else ""
46
+ return f"IMG({f.get('imageRef','')[:8]}{flag})"
47
+ if t and "GRADIENT" in t:
48
+ return "GRADIENT"
49
+ return ""
50
+
51
+
52
+ def walk(n, depth=0):
53
+ if n.get("visible") is False: # hidden variants hold stale copy — skip
54
+ return
55
+ if depth > maxd:
56
+ return
57
+ bb = n.get("absoluteBoundingBox") or {}
58
+ if not bb:
59
+ return
60
+ x, y = int(bb.get("x", 0) - ox), int(bb.get("y", 0) - oy)
61
+ w, h = int(bb.get("width", 0)), int(bb.get("height", 0))
62
+ t = n.get("type", "")
63
+ pad = " " * depth
64
+
65
+ if t == "TEXT":
66
+ s = n.get("style", {})
67
+ chars = (n.get("characters") or "").replace("\n", "⏎").replace("\r", "")[:58]
68
+ bits = [f'{s.get("fontFamily")} {s.get("fontWeight")} {round(s.get("fontSize",0))}px']
69
+ if s.get("lineHeightPx"): bits.append(f'lh{round(s["lineHeightPx"])}')
70
+ if s.get("letterSpacing"): bits.append(f'ls{s["letterSpacing"]:.1f}')
71
+ if s.get("textCase"): bits.append(s["textCase"]) # UPPER is real
72
+ if s.get("textAlignHorizontal"): bits.append(s["textAlignHorizontal"])
73
+ c = paint(n)
74
+ if c: bits.append(c)
75
+ print(f'{pad}TEXT {x:>5},{y:<5} {w:>4}x{h:<4} "{chars}" [{" ".join(bits)}]')
76
+ else:
77
+ bits = []
78
+ f = paint(n)
79
+ if f: bits.append(f)
80
+ st = paint(n, "strokes")
81
+ if st: bits.append(f'stroke {st} {n.get("strokeWeight","")}')
82
+ if n.get("cornerRadius"): bits.append(f'r{int(n["cornerRadius"])}')
83
+ if n.get("layoutMode"): bits.append(f'{n["layoutMode"].lower()} gap{int(n.get("itemSpacing",0))}')
84
+ pads = [n.get(k) for k in ("paddingLeft", "paddingTop", "paddingRight", "paddingBottom")]
85
+ if any(pads): bits.append("pad " + "/".join(str(int(p or 0)) for p in pads))
86
+ name = (n.get("name") or "")[:30]
87
+ print(f'{pad}{t[:5]:<5} {x:>5},{y:<5} {w:>4}x{h:<4} {name}{" ["+" ".join(bits)+"]" if bits else ""}')
88
+
89
+ for c in n.get("children", []) or []:
90
+ walk(c, depth + 1)
91
+
92
+
93
+ walk(doc)