95 lines
3.4 KiB
Python
95 lines
3.4 KiB
Python
#!/usr/bin/env python3
|
||
"""
|
||
build-sprite.py — يبني sprite sheet لـ MapLibre من مجلد SVG.
|
||
|
||
لماذا: MapLibre لا يقرأ ملفات SVG مفردة. يحتاج صورة PNG واحدة تُرصف فيها كل
|
||
الآيقونات + فهرس JSON يحدد موضع وحجم كل واحدة. كان الستايل يشير إلى
|
||
demotiles.maplibre.org (خادم تجريبي)، وحين يفشل تحميله يرسم MapLibre النص
|
||
بلا آيقونة وبصمت — فتبدو الخريطة بأسماء بلا رموز.
|
||
|
||
الاستخدام:
|
||
pip install cairosvg pillow
|
||
python3 infrastructure/scripts/build-sprite.py
|
||
|
||
المخرجات في apps/web/public/:
|
||
sprite.png / sprite.json (1x)
|
||
sprite@2x.png / sprite@2x.json (شاشات Retina — MapLibre يطلبها تلقائياً)
|
||
|
||
ثم في الستايل: "sprite": "https://map-saas.intaleqapp.com/sprite"
|
||
(بلا امتداد — MapLibre يضيف .png/.json و@2x بنفسه)
|
||
"""
|
||
import json
|
||
import os
|
||
import sys
|
||
|
||
try:
|
||
import cairosvg
|
||
from PIL import Image
|
||
except ImportError:
|
||
sys.exit("ينقص: pip install cairosvg pillow")
|
||
|
||
ROOT = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
ICONS_DIR = os.path.join(ROOT, "apps", "web", "public", "icons")
|
||
OUT_DIR = os.path.join(ROOT, "apps", "web", "public")
|
||
|
||
BASE_SIZE = 24 # بكسل للآيقونة عند 1x
|
||
PADDING = 2 # فاصل يمنع نزّ ألوان الجيران عند التصغير (bleeding)
|
||
|
||
|
||
def build(scale: int, suffix: str) -> None:
|
||
size = BASE_SIZE * scale
|
||
pad = PADDING * scale
|
||
|
||
svgs = sorted(f for f in os.listdir(ICONS_DIR) if f.endswith(".svg"))
|
||
if not svgs:
|
||
sys.exit(f"لا توجد ملفات SVG في {ICONS_DIR}")
|
||
|
||
rendered = []
|
||
for fname in svgs:
|
||
name = os.path.splitext(fname)[0]
|
||
png_bytes = cairosvg.svg2png(
|
||
url=os.path.join(ICONS_DIR, fname),
|
||
output_width=size,
|
||
output_height=size,
|
||
)
|
||
import io
|
||
rendered.append((name, Image.open(io.BytesIO(png_bytes)).convert("RGBA")))
|
||
|
||
# شبكة مربعة تقريباً: أقل هدراً من صف واحد طويل
|
||
cols = max(1, int(len(rendered) ** 0.5 + 0.999))
|
||
rows = (len(rendered) + cols - 1) // cols
|
||
|
||
sheet = Image.new(
|
||
"RGBA",
|
||
(cols * (size + pad) - pad, rows * (size + pad) - pad),
|
||
(0, 0, 0, 0),
|
||
)
|
||
|
||
index = {}
|
||
for i, (name, img) in enumerate(rendered):
|
||
x = (i % cols) * (size + pad)
|
||
y = (i // cols) * (size + pad)
|
||
sheet.paste(img, (x, y))
|
||
index[name] = {
|
||
"x": x, "y": y,
|
||
"width": size, "height": size,
|
||
"pixelRatio": scale,
|
||
"sdf": False,
|
||
}
|
||
|
||
sheet.save(os.path.join(OUT_DIR, f"sprite{suffix}.png"))
|
||
with open(os.path.join(OUT_DIR, f"sprite{suffix}.json"), "w") as fh:
|
||
json.dump(index, fh, indent=2)
|
||
|
||
print(f"✅ sprite{suffix}: {len(index)} آيقونة، {sheet.width}×{sheet.height}px")
|
||
return list(index)
|
||
|
||
|
||
if __name__ == "__main__":
|
||
names = build(1, "")
|
||
build(2, "@2x")
|
||
print("\nالآيقونات المتاحة لـ icon-image:")
|
||
print(" " + ", ".join(names))
|
||
print("\n⚠️ طبقات الستايل تطلب أسماء قد لا تكون موجودة أعلاه")
|
||
print(" (college, rail, tourist ...) — راجعها أو أضف SVG مقابلاً لكل ناقص.")
|