-
-
Notifications
You must be signed in to change notification settings - Fork 24
Open
Description
I noticed that nested frames were not handled recursively (only top frames within frames were handled), so I generated this code to replace parse_file() function:
def parse_file(file, token, download_images=True, out=None):
output = []
result = get_file(file, token)
if isinstance(result, tuple):
return []
try:
frames = result['document']['children'][0]['children']
frame_count = 1 if len(frames) > 1 else 0
def parse_frame(frame, frame_count):
nonlocal output
parsed = []
image_count = 0
entry_placeholder = False
text_placeholder = False
# core.py — inside parse_file, in parse_frame(frame, frame_count)
def flatten_children(node):
stack = [node]
while stack:
n = stack.pop()
for child in n.get('children', []):
# Visit the child and keep diving
yield child
if child.get('children'):
stack.append(child)
items = ["image", "button", "label", "scale", "listbox", "textbox", "textarea", "rectangle", "spinbox", "circle", "oval", "line"]
container_types = {"FRAME", "GROUP", "INSTANCE", "COMPONENT", "SECTION", "CANVAS"}
for i in flatten_children(frame):
# Skip container nodes unless their name explicitly marks them as a UI item
name_prefix = i.get('name', '').split(' ', 1)[0].lower()
figma_type = i.get('type', '')
if figma_type in container_types and name_prefix not in items:
print(f"[parse_file] skip container: {i.get('name')} ({figma_type}) id={i.get('id')}")
continue
# Bounds guard: some invisible text nodes have no absoluteRenderBounds
if 'absoluteBoundingBox' in i and i['absoluteBoundingBox']:
bounds = i['absoluteBoundingBox']
elif i.get('absoluteRenderBounds'):
bounds = i['absoluteRenderBounds']
else:
print(f"[parse_file] skip node with no bounds: {i.get('name')} id={i.get('id')}")
continue
# Relative positions to the root frame
i['x'] = abs(int(frame['absoluteBoundingBox']['x']) - int(bounds['x']))
i['y'] = abs(int(frame['absoluteBoundingBox']['y']) - int(bounds['y']))
i['width'] = int(bounds['width'])
i['height'] = int(bounds['height'])
# Classify by name prefix (existing behavior), default to text
node_type = name_prefix if name_prefix in items else "text"
i['type'] = node_type
# Colors
i['background'] = None
bg_color = i.get('backgroundColor') or \
(i.get('background', [{}])[0].get('color') if i.get('background') else None) or \
(i.get('fills', [{}])[0].get('color') if i.get('fills') else None)
if bg_color:
i['background'] = rgb_to_hex(bg_color['r'], bg_color['g'], bg_color['b'])
fg = get_foreground_color(bg_color['r'], bg_color['g'], bg_color['b'])
i['foreground'] = '#ffffff' if fg == i['background'] and fg == '#000000' else ('#000000' if fg == i['background'] and fg == '#ffffff' else fg)
else:
i['background'] = "#000000"
i['foreground'] = "#FFFFFF"
# Strokes
if i.get('strokes'):
stroke_color = i.get('strokes', [{}])[0].get('color')
if stroke_color:
i['stroke_color'] = rgb_to_hex(stroke_color['r'], stroke_color['g'], stroke_color['b'])
# Per-type details (keep existing logic)
if i['type'] in ['text', 'label']:
i['text'] = i.get('characters', '').replace('\n', '\\n')
style = i.get('style', {}) # payload text style exists [payload.py](/api/storage/attachment/b9620458-e75e-4abb-82a0-e7c0cda79b78/payload.py)【2-7】
i['font'] = style.get('fontFamily', 'Default Font')
i['font_size'] = int(style.get('fontSize', 12))
elif i['type'] == 'image':
parts = i.get('name', '').split(' ')
name = " ".join(parts[1:]).strip()
if not name:
image_count += 1
name = str(image_count)
# download_image knows how to handle frame_count
img = download_image(file, i['id'], name, token, out, frame_count) if frame_count > 0 else download_image(file, i['id'], name, token, out)
i['image'] = img if img else None
elif i['type'] == 'scale':
scale = i.get('name', '').split(' ')
if len(scale) >= 3:
i['from'] = int(scale[1])
i['to'] = int(scale[2])
i['orient'] = scale[3] if len(scale) > 3 else "HORIZONTAL"
elif i['type'] in ['textbox', 'textarea']:
parts = i.get('name', '').split(' ')
placeholder = " ".join(parts[1:]).strip()
if placeholder:
i['placeholder'] = placeholder
if i['type'] == 'textbox':
entry_placeholder = True
else:
text_placeholder = True
elif i['type'] == 'button' and download_images:
image_count += 1
img = download_image(file, i['id'], str(image_count), token, out, frame_count) if frame_count > 0 else download_image(file, i['id'], str(image_count), token, out)
i['image'] = img if img else None
parsed.append(i)
print(f"[parse_file] ok: {i.get('name')} -> {i['type']} id={i.get('id')} x={i['x']} y={i['y']} w={i['width']} h={i['height']}")
frame_bg = frame.get('backgroundColor') or \
(frame.get('background', [{}])[0].get('color') if frame.get('background') else None) or \
(frame.get('fills', [{}])[0].get('color') if frame.get('fills') else None)
if frame_bg:
frame_bg = rgb_to_hex(frame_bg['r'], frame_bg['g'], frame_bg['b'])
else:
frame_bg = "No background color specified"
output.append([parsed, [
int(frame['absoluteBoundingBox']['width']),
int(frame['absoluteBoundingBox']['height']),
frame_bg,
result['name'].replace('\n', '\\n'),
frame_count,
entry_placeholder,
text_placeholder
]])
threads = []
for frame in frames:
if frame["type"] == "FRAME":
thread = threading.Thread(target=parse_frame, args=(frame, frame_count,))
threads.append(thread)
thread.start()
frame_count = frame_count + 1;
for thread in threads:
thread.join()
except KeyError as e:
print(f"KeyError: {str(e)} - likely due to missing keys in JSON response")
return output
Seems to work, so thought I'd share.
Probably don't need the print statements, but maybe leave on if a --verbose flag is triggered or something? Just an idea.
Metadata
Metadata
Assignees
Labels
No labels