Download core/crello.py from FineEnvs/DesignGym: direct link, hf CLI and curl.
- Browser
- Download file 10.4 kB
-
https://huggingface.co/spaces/FineEnvs/DesignGym/resolve/main/core/crello.py
- Command line
-
hf download hf://spaces/FineEnvs/DesignGym/core/crello.py
-
curl -L -o crello.py https://huggingface.co/spaces/FineEnvs/DesignGym/resolve/main/core/crello.py
10.4 kB
| """Crello row -> ground-truth document + the assets a model gets to work with. | |
| What survives the conversion, and why: | |
| - Image, SVG and SVG-mask layers become `image` elements over pre-rendered | |
| assets. Their colours are baked into the PNG, exactly as a stock asset would | |
| arrive in a real editor. | |
| - A `ColoredBackground` that is one flat colour becomes the document | |
| background; a textured or gradient one stays an asset. | |
| - Text layers become real `text` elements. Crello's pre-rendered text PNGs are | |
| withheld from the assets, otherwise the cheapest policy is to paste pictures | |
| of the words instead of typing them. | |
| - Per-character colour, bold and italic become runs. Crello's explicit | |
| `text_line` indices become `\\n` breaks, and the raw newlines it says to | |
| ignore are dropped. | |
| - Identical assets are merged, so a design that uses one divider twice asks for | |
| the same asset to be placed twice rather than rewarding a coin-flip over two | |
| indistinguishable ids. | |
| """ | |
| from __future__ import annotations | |
| import hashlib | |
| import io | |
| import random | |
| from collections import Counter | |
| from dataclasses import dataclass, field | |
| import numpy as np | |
| from PIL import Image | |
| from .colors import parse_color | |
| from .doc import DesignDoc, Element, Run | |
| from .fonts import FontRegistry | |
| from .text import fit_box | |
| _QUANTISED = ("x", "y", "width", "height", "angle", "opacity", "font_size", "line_height", | |
| "letter_spacing") | |
| ASSET_KIND = { | |
| "ImageElement": "photo", | |
| "SvgMaskElement": "photo", | |
| "SvgElement": "graphic", | |
| "ColoredBackground": "background", | |
| } | |
| class ConvertedDesign: | |
| crello_id: str | |
| doc: DesignDoc | |
| assets: dict[str, Image.Image] | |
| asset_meta: dict[str, dict] | |
| meta: dict | |
| preview: Image.Image | None | |
| warnings: list[str] = field(default_factory=list) | |
| def _label(features, key: str, value): | |
| """ClassLabel int (or list of ints) -> name, tolerant of already-decoded rows.""" | |
| feat = features[key] | |
| inner = getattr(feat, "feature", feat) | |
| names = getattr(inner, "names", None) | |
| if names is None: | |
| return value | |
| if isinstance(value, list): | |
| return [names[v] if isinstance(v, int) else v for v in value] | |
| return names[value] if isinstance(value, int) else value | |
| def _to_pil(value) -> Image.Image | None: | |
| if value is None: | |
| return None | |
| if isinstance(value, Image.Image): | |
| return value | |
| if isinstance(value, dict) and value.get("bytes"): | |
| return Image.open(io.BytesIO(value["bytes"])) | |
| return None | |
| def _flat_color(img: Image.Image) -> str | None: | |
| arr = np.asarray(img.convert("RGBA").resize((32, 32)), dtype=np.float32) | |
| opaque = arr[:, :, 3] > 250 | |
| if opaque.mean() < 0.98: | |
| return None | |
| rgb = arr[:, :, :3][opaque] | |
| if rgb.std(axis=0).max() > 2.5: | |
| return None | |
| r, g, b = (int(round(v)) for v in rgb.mean(axis=0)) | |
| return f"#{r:02x}{g:02x}{b:02x}" | |
| def _text_runs(text: str, colors, bolds, italics, lines) -> tuple[str, list[dict]]: | |
| """Rebuild text with explicit line breaks and per-character style runs.""" | |
| def at(seq, i, default): | |
| try: | |
| return seq[i] | |
| except (IndexError, TypeError): | |
| return default | |
| out_chars: list[tuple[str, str, bool, bool]] = [] | |
| prev_line = None | |
| for i, ch in enumerate(text): | |
| if ch == "\n": | |
| continue | |
| line = at(lines, i, prev_line or 0) | |
| style = ( | |
| parse_color(at(colors, i, "#000000") or "#000000"), | |
| bool(at(bolds, i, False)), | |
| bool(at(italics, i, False)), | |
| ) | |
| if prev_line is not None and line != prev_line: | |
| out_chars.append(("\n", *out_chars[-1][1:]) if out_chars else ("\n", *style)) | |
| out_chars.append((ch, *style)) | |
| prev_line = line | |
| runs: list[dict] = [] | |
| for ch, color, bold, italic in out_chars: | |
| if runs and (runs[-1]["color"], runs[-1]["bold"], runs[-1]["italic"]) == ( | |
| color, | |
| bold, | |
| italic, | |
| ): | |
| runs[-1]["text"] += ch | |
| else: | |
| runs.append({"text": ch, "color": color, "bold": bold, "italic": italic}) | |
| return "".join(r["text"] for r in runs), runs | |
| def convert_row(row: dict, features, fonts: FontRegistry) -> ConvertedDesign: | |
| crello_id = str(row["id"]) | |
| width, height = int(row["canvas_width"]), int(row["canvas_height"]) | |
| types = _label(features, "type", list(row["type"])) | |
| font_names = _label(features, "font", list(row["font"])) | |
| aligns = _label(features, "text_align", list(row["text_align"])) | |
| doc = DesignDoc(width=width, height=height, background="#ffffff") | |
| warnings: list[str] = [] | |
| raw_assets: list[tuple[str, Image.Image, str]] = [] # (element slot, image, kind) | |
| pending: list[tuple[int, dict]] = [] # element kwargs, with asset slot names | |
| bg_set = False | |
| for i in range(int(row["length"])): | |
| etype = types[i] | |
| x, y = float(row["left"][i]), float(row["top"][i]) | |
| w, h = float(row["width"][i]), float(row["height"][i]) | |
| angle = float(row["angle"][i] or 0.0) | |
| opacity = float(row["opacity"][i]) if row["opacity"][i] is not None else 1.0 | |
| if w < 1 or h < 1 or opacity <= 0.01: | |
| warnings.append(f"dropped degenerate element {i} ({etype})") | |
| continue | |
| common = dict(x=x, y=y, width=w, height=h, angle=angle, opacity=min(1.0, opacity)) | |
| if etype == "TextElement": | |
| text = str(row["text"][i] or "") | |
| if not text.strip(): | |
| continue | |
| family_raw = font_names[i] or "" | |
| if not fonts.knows(family_raw): | |
| warnings.append(f"font fallback: {family_raw!r}") | |
| family = fonts.resolve(family_raw) | |
| joined, runs = _text_runs( | |
| text, | |
| row["text_color"][i], | |
| row["font_bold"][i], | |
| row["font_italic"][i], | |
| row["text_line"][i], | |
| ) | |
| counts = Counter((r["color"], r["bold"], r["italic"]) for r in runs for _ in r["text"]) | |
| (color, bold, italic), _ = counts.most_common(1)[0] | |
| uniform = len({(r["color"], r["bold"], r["italic"]) for r in runs}) == 1 | |
| el_runs = None | |
| if not uniform: | |
| el_runs = [ | |
| Run( | |
| text=r["text"], | |
| color=None if r["color"] == color else r["color"], | |
| bold=None if r["bold"] == bold else r["bold"], | |
| italic=None if r["italic"] == italic else r["italic"], | |
| ) | |
| for r in runs | |
| ] | |
| align = aligns[i] if aligns[i] in ("left", "center", "right") else "left" | |
| kwargs = dict( | |
| kind="text", | |
| text=joined, | |
| font_family=family, | |
| font_size=float(row["font_size"][i] or 24.0), | |
| color=color, | |
| bold=bold, | |
| italic=italic, | |
| align=align, | |
| line_height=float(row["line_height"][i] or 1.0), | |
| letter_spacing=float(row["letter_spacing"][i] or 0.0), | |
| uppercase=bool(row["capitalize"][i]), | |
| runs=el_runs, | |
| **common, | |
| ) | |
| pending.append((i, kwargs)) | |
| continue | |
| img = _to_pil(row["image"][i]) | |
| if img is None: | |
| warnings.append(f"element {i} ({etype}) has no image") | |
| continue | |
| if etype == "ColoredBackground" and not bg_set: | |
| flat = _flat_color(img) | |
| colors = row["color"][i] or [] | |
| if flat is not None: | |
| doc.background = parse_color(colors[0]) if colors else flat | |
| bg_set = True | |
| continue | |
| slot = f"s{i}" | |
| raw_assets.append((slot, img.convert("RGBA"), ASSET_KIND.get(etype, "graphic"))) | |
| pending.append((i, dict(kind="image", asset_id=slot, **common))) | |
| # Merge identical assets, then hand out ids in a shuffled order so the id | |
| # sequence does not leak the stacking order of the ground truth. | |
| digest_to_slot: dict[str, str] = {} | |
| slot_alias: dict[str, str] = {} | |
| unique: list[tuple[str, Image.Image, str]] = [] | |
| for slot, img, kind in raw_assets: | |
| digest = hashlib.sha1(img.tobytes() + str(img.size).encode()).hexdigest() | |
| if digest in digest_to_slot: | |
| slot_alias[slot] = digest_to_slot[digest] | |
| continue | |
| digest_to_slot[digest] = slot | |
| slot_alias[slot] = slot | |
| unique.append((slot, img, kind)) | |
| order = list(range(len(unique))) | |
| random.Random(int(hashlib.sha1(crello_id.encode()).hexdigest()[:8], 16)).shuffle(order) | |
| slot_to_id = {unique[k][0]: f"a{n + 1}" for n, k in enumerate(order)} | |
| assets: dict[str, Image.Image] = {} | |
| asset_meta: dict[str, dict] = {} | |
| for slot, img, kind in unique: | |
| aid = slot_to_id[slot] | |
| assets[aid] = img | |
| asset_meta[aid] = { | |
| "id": aid, | |
| "kind": kind, | |
| "aspect": round(img.width / img.height, 4), | |
| } | |
| for _, kwargs in pending: | |
| if kwargs["kind"] == "image": | |
| kwargs["asset_id"] = slot_to_id[slot_alias[kwargs["asset_id"]]] | |
| # Crello stores float32. Quantise to what a tool call can express, so | |
| # the expert trajectory reproduces the ground truth exactly. | |
| for key in _QUANTISED: | |
| if key in kwargs and isinstance(kwargs[key], float): | |
| kwargs[key] = round(kwargs[key], 2) | |
| el = Element(id=doc.new_id(), **kwargs) | |
| if el.kind == "text": | |
| _, el.height = fit_box(el, fonts) | |
| doc.elements.append(el) | |
| meta = { | |
| "crello_id": crello_id, | |
| "title": str(row.get("title") or ""), | |
| "format": _label(features, "format", row["format"]), | |
| "category": _label(features, "category", row["category"]), | |
| "keywords": list(row.get("keywords") or []), | |
| "industries": _label(features, "industries", list(row.get("industries") or [])), | |
| "canvas": {"width": width, "height": height}, | |
| } | |
| return ConvertedDesign( | |
| crello_id=crello_id, | |
| doc=doc, | |
| assets=assets, | |
| asset_meta=asset_meta, | |
| meta=meta, | |
| preview=_to_pil(row.get("preview")), | |
| warnings=warnings, | |
| ) | |