Facebook Page: profile pictures, three covers, preview page and texts (marketing/facebook); R-916 opened
gates / gates (push) Successful in 4m24s

build.py (Pillow, site tokens + Plus Jakarta Sans) with in-build checks: profile circle (today's shape 6823 pixels
outside, new 0), cover safe area and profile-circle clearance, sizes read back. COPY.md: intro 99/100, long
description, Messenger welcome, three first posts, each claim traced to the website. R-914 gets the listing-based
removal proof and the picture-API tasks; R-915 the footer-link note; R-916 the logo's vector master.
This commit is contained in:
2026-10-08 19:19:50 +02:00
parent e9e6300cfc
commit f0dc6a9d4e
14 changed files with 670 additions and 8 deletions
+13
View File
@@ -0,0 +1,13 @@
# marketing — CHANGELOG
## facebook — the Page's pictures and texts (2026-10-08)
- `marketing/facebook/build.py` (Pillow only) makes `out/profile-dark.png` + `out/profile-white.png` (932 x 932, the
logo PNG placed unscaled), three covers `out/cover-{a,b,c}.png` (1640 x 624) and `out/preview.html` (self-contained).
Checks in the build: profile circle (today's shape 6823 pixels outside radius 0.42 x width → new pictures 0), cover
safe area + profile-circle clearance (content layer only), sizes read back from the files. The site's tokens and font.
- `COPY.md`: intro (99 of 100 characters), long description, Messenger welcome, three first posts — each claim cites its
website page; no price.
- Fixed in the session: covers B and C first failed the safe-area check (text in the phone profile circle's corner, the
frame past the phone crop) and were moved; `logo.svg` renders with the wrong lettering here, so the PNG is the master
(R-916).
+85
View File
@@ -0,0 +1,85 @@
# Felhom.eu Facebook Page — texts to paste
Every claim cites the website page it comes from (`<!-- forrás: … -->`, not pasted). No price, no promise the
site does not make. Prices live only on the non-public services page and stay off Facebook.
## 1. Rövid leírás (Bemutatkozás / Intro — legfeljebb 100 karakter)
<!-- forrás: index.html hero („Saját felhőd, saját szabályaid", „Te irányítasz, mi segítünk") -->
<!-- limit: Graph API Page `about` = 100 karakter (developers.facebook.com/docs/graph-api/reference/page/) -->
```
Saját felhőd, saját szabályaid. Otthoni szerver telepítés, üzemeltetés. Te irányítasz, mi segítünk.
```
## 2. Hosszabb leírás (Névjegy / Részletek)
<!-- forrás: index.html — „Mit tud a doboz ma?", „Miért válaszd a saját szervered?", „Zárt teszt", kapcsolat -->
```
A Felhom egy otthoni szerver, ami nálad van: a fotóid, a dokumentumaid és a médiatárad a saját gépeden maradnak, nem egy nagy techcég szerverén.
Mit tud ma a doboz:
– A telefonod fotói automatikusan feltöltődnek a saját gépedre.
– A beszkennelt papírjaidat a rendszer felismeri és kereshetővé teszi.
– A doboz mappái megjelennek a Windows Intézőben és a Mac Finderben.
– Minden éjjel mentés készül: a dobozon, egy második meghajtón (ha van), és titkosítva egy távoli tárhelyen.
– A távoli mentés kulcsa egy helyreállítási kód, amit csak te ismersz.
– 56 alkalmazás telepíthető pár kattintással, parancssor nélkül.
– Ha elmegy a net, otthon továbbra is eléred a fotóidat és a fájljaidat.
Mi figyeljük a doboz állapotát, és frissítjük a rendszert. Te csak használod.
Most néhány magyar háztartással zárt tesztet indítunk. Ha érdekel, írj: https://felhom.eu/kapcsolat
```
## 3. Messenger üdvözlő üzenet
<!-- forrás: index.html „Zárt teszt" („nem kötelez semmire, először csak beszélgetünk") -->
```
Szia! Köszi, hogy írtál a Felhomnak. Írd meg, mi érdekel – a zárt tesztről vagy az otthoni szerverről –, és válaszolunk.
```
## 4. Az első három bejegyzés
### 4.1 Mi a Felhom? — kép: `out/cover-b.png`
<!-- forrás: index.html hero; „Mit tud a doboz ma?" (fotók, papírok, mappák, mentés); „Zárt teszt" -->
```
Saját felhőd, saját szabályaid.
A Felhom egy otthoni szerver, amit mi telepítünk és üzemeltetünk – te pedig használod. A telefonod fotói maguktól feltöltődnek rá, a beszkennelt papírjaid kereshetők lesznek, a mappái pedig ott vannak a Windows Intézőben vagy a Mac Finderben.
Most néhány magyar háztartással zárt tesztet kezdünk. Ha kipróbálnád, írj nekünk: https://felhom.eu/kapcsolat
```
### 4.2 Az adataid otthon maradnak — kép: `out/cover-c.png`
<!-- forrás: index.html „Teljes adatvédelem", „Helyreállítási kód, amit csak te ismersz", „Háromszintű automatikus mentés", „Internetkimaradáskor is működik" -->
```
Hol vannak most a családi fotóid?
A Felhommal a te otthonodban. Nem a Google vagy a Microsoft szerverein, hanem a saját gépeden.
Minden éjjel mentés készül: a dobozon, egy második meghajtón (ha van), és egy titkosított másolat egy távoli tárhelyre. A titkosítás kulcsa egy helyreállítási kód, ami egyszer jelenik meg – a te képernyődön. Mi nem ismerjük, és nem tudjuk visszaállítani, ezért tedd el jól.
És ha elmegy a net? Otthon továbbra is eléred a fotóidat és a fájljaidat.
https://felhom.eu
```
### 4.3 Milyen alkalmazásokat futtathatsz? — kép: `out/cover-a.png`
<!-- forrás: index.html „56 telepíthető alkalmazás" és a kategóriák listája; alkalmazasok.html (a galéria) -->
```
56 alkalmazás, pár kattintással – parancssor nélkül.
Fotók és média: Immich, Jellyfin, Plex, Audiobookshelf. Fájlok: Nextcloud, FileBrowser. Dokumentumok: Paperless-ngx, BookStack. Jelszavak: Vaultwarden. Otthon: Mealie, Homebox, Actual Budget.
Szinte mind nyílt forráskódú, és mind a te gépeden fut. Nézd meg mindet, képekkel:
https://felhom.eu/alkalmazasok
```
+51
View File
@@ -0,0 +1,51 @@
# marketing/facebook — the Felhom.eu Facebook Page's pictures and texts
Files the operator uploads and pastes by hand. Nothing here talks to Facebook (the robot posting path is R-914).
## Rebuild
```
python3 marketing/facebook/build.py
```
One command makes every PNG and `out/preview.html`, and runs the checks: the profile circle (no logo pixel
outside 0.42 x width; shown failing first on today's shape), the cover safe area (content inside what computer
AND phone show, clear of the profile circle), and the sizes read back from the files. Exit 0 = all passed.
Needs Python 3 + Pillow only. Brand source: `website/assets/logo.png` (the logo master — see below),
`website/assets/site.css` tokens, `website/assets/fonts/` (Plus Jakarta Sans).
## What goes where
| File | Where on the Page |
|---|---|
| `out/profile-dark.png` (recommended) or `out/profile-white.png` | Profile picture |
| `out/cover-a.png` / `cover-b.png` / `cover-c.png` — pick one | Cover photo |
| `COPY.md` §1 | Intro / Bemutatkozás (≤ 100 characters) |
| `COPY.md` §2 | About → details (longer description) |
| `COPY.md` §3 | Messenger welcome message |
| `COPY.md` §4 | The first three posts, each with its picture |
Open `out/preview.html` (download it from Gitea, open locally — it is self-contained) to see each picture the way
Facebook shows it.
## Sizes used (READ, second-hand)
Meta's own help page (facebook.com/help/125379114252045) needs a browser and could not be read from DooPlex.
Figures from size guides that quote it: cover shown **820 x 312** on computers and **640 x 360** on phones
(built at 2 x: 1640 x 624); profile picture shown about 176 px; profile circle over the cover at 16 px / 176 px
(computer) and 24 px / 196 px (phone). Sources: postfa.st/sizes/facebook/cover,
linearity.io/templates/facebook-size-guide, fasturtle.com (faq-items/what-size-photos-do-i-use-on-facebook).
Page `about` = 100 characters: developers.facebook.com/docs/graph-api/reference/page/ (Meta, first-hand).
## The logo master
`website/assets/logo.svg` is **not** usable as a master: its „felhom.eu" lettering is live text in the fonts
„M+ 2c" and „Vremena Grotesk", which DooPlex does not have, so any renderer here draws other letters. The PNG
(645 x 408) is placed unscaled, so the profile picture is 932 x 932 — sharp, not enlarged. A vector master with the
lettering converted to paths (Inkscape: Path → Object to Path, on the machine that has the fonts) is R-916.
## Cover C
Uses `website/assets/dashboard-backups-hu.webp` (demo box, already public on the website). The demo box's domain
chip next to the title is covered with the page colour in the copy; the source file is unchanged. No third-party
logo appears in it (app names are text). The Launcher and Apps pictures were not used: they show app logos.
+430
View File
@@ -0,0 +1,430 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""build.py — the Felhom.eu Facebook Page pictures: two profile pictures, three covers, one preview page.
Run: python3 marketing/facebook/build.py
Exit: 0 every image built and every check passed · 1 a check failed · 2 a render failed
Renderer: Pillow only (shapes, text, compositing). Text is set in the website's own Plus Jakarta Sans
(website/assets/fonts/*.woff2), which Pillow reads through FreeType. NOT librsvg: tried 2026-10-08 — pango fell back
to DejaVu for the woff2 fonts, and logo.svg's own lettering is live <text> in 'M+ 2c' / 'Vremena Grotesk', fonts
DooPlex does not have, so the SVG renders with the wrong letters. The logo master is therefore website/assets/logo.png
(645 x 408, the file the site itself shows), placed unscaled.
Checks (each prints its numbers; a failure exits 1):
circle every logo pixel of a profile picture lies inside radius 0.42 x width of the centre (8 % margin).
Shown failing first on the shape the Page has today (logo.png, full size, on a square).
safe every pixel of a cover's CONTENT layer (text, logo, drawings — not the background) lies inside the
area both desktop and phone show, and outside the places the profile circle covers.
size each PNG's size is read back from the file, never taken from the constants here.
Sizes (graded READ, second-hand — Meta's own help page needs a browser; see README.md for the URLs):
cover shown 820 x 312 on computers, 640 x 360 on phones; built at 2 x = 1640 x 624.
profile shown 176 px (computer) / 196 px (phone, older layouts) — built at the size where logo.png fits unscaled.
profile circle over the cover: computer 16 px from the left, 176 px from the top (176 px circle);
phone 24 px from the left, 196 px from the top (140 px circle).
"""
import base64
import math
import os
import sys
from PIL import Image, ImageDraw, ImageFilter, ImageFont
HERE = os.path.dirname(os.path.abspath(__file__))
ROOT = os.path.dirname(os.path.dirname(HERE))
sys.path.insert(0, HERE)
ASSETS = os.path.join(ROOT, "website", "assets")
OUT = os.path.join(HERE, "out")
LOGO_PNG = os.path.join(ASSETS, "logo.png")
SHOT = os.path.join(ASSETS, "dashboard-backups-hu.webp")
PJS = os.path.join(ASSETS, "fonts", "pjs-latin.woff2")
PJS_EXT = os.path.join(ASSETS, "fonts", "pjs-latin-ext.woff2")
def hexc(h, a=255):
h = h.lstrip("#")
return (int(h[0:2], 16), int(h[2:4], 16), int(h[4:6], 16), a)
# website/assets/site.css :root — design system v2 tokens, verbatim (the site is dark-only).
BG0, BG1, BG2 = hexc("#0A1220"), hexc("#0F1B2E"), hexc("#16263F")
LINE = hexc("#22344F")
TEXT1, TEXT2, TEXT3 = hexc("#EDF2F9"), hexc("#94A6BF"), hexc("#5E7392")
BLUE, BLUE_BRIGHT = hexc("#0083D8"), hexc("#2EA8F5")
WHITE = hexc("#FFFFFF")
# The site's headline (index.html hero), from escapes (brief section 9.7).
L1 = "Saj\u00e1t felh\u0151d,"
L2 = "saj\u00e1t szab\u00e1lyaid"
SLOGAN = L1 + " " + L2
DOMAIN = "felhom.eu"
CIRCLE_R = 0.42 # brief §7 A: the check radius, as a fraction of the width
LOGO_R = 0.40 # where the logo's farthest pixel is placed (inside the check radius)
CW, CH = 1640, 624 # cover, 2 x the 820 x 312 computer display
SCALE = CW / 820.0
# Phone shows the full height at 640:360 → the centre CW_PHONE pixels of the width.
CW_PHONE = CH * 640.0 / 360.0
PHONE_X0 = (CW - CW_PHONE) / 2.0
MARGIN = 40
SAFE = (int(math.ceil(PHONE_X0 + MARGIN)), MARGIN, int(CW - PHONE_X0 - MARGIN), CH - MARGIN)
PAD = 24
DESK_CIRCLE = (16 * SCALE, 176 * SCALE, 176 * SCALE) # x, y, diameter in cover pixels
PHONE_K = CH / 360.0
PHONE_CIRCLE = (PHONE_X0 + 24 * PHONE_K, 196 * PHONE_K, 140 * PHONE_K)
QUIET = [(0, c[1] - PAD, c[0] + c[2] + PAD, CH) for c in (DESK_CIRCLE, PHONE_CIRCLE)]
FAILS = []
def check(ok, msg):
print((" ok " if ok else " FAIL ") + msg)
if not ok:
FAILS.append(msg)
# ------------------------------------------------------------------ text in the site font
_fonts = {}
def font(size, weight, ext):
key = (size, weight, ext)
if key not in _fonts:
f = ImageFont.truetype(PJS_EXT if ext else PJS, size)
f.set_variation_by_name(weight)
_fonts[key] = f
return _fonts[key]
def runs(text):
"""Split into runs by the CSS unicode-range of the two woff2 files (latin-ext = U+0100–02BA)."""
out = []
for ch in text:
ext = 0x100 <= ord(ch) <= 0x2BA
if out and out[-1][0] == ext:
out[-1][1] += ch
else:
out.append([ext, ch])
return out
def text_width(text, size, weight):
return sum(font(size, weight, e).getlength(s) for e, s in runs(text))
def draw_text(img, x, y, text, size, weight, fill, align="left"):
"""Draw at baseline y; x is the left edge, or the centre with align='center'. Returns the width."""
w = text_width(text, size, weight)
if align == "center":
x -= w / 2.0
d = ImageDraw.Draw(img)
for e, s in runs(text):
f = font(size, weight, e)
d.text((x, y), s, font=f, fill=fill, anchor="ls")
x += f.getlength(s)
return w
def fit_size(text, weight, max_w, start):
size = start
while text_width(text, size, weight) > max_w:
size -= 1
return size
# ------------------------------------------------------------------ the logo
def alpha_points(img, thr=16):
a = img.getchannel("A")
w, h = img.size
px = a.load()
return [(x, y) for y in range(h) for x in range(w) if px[x, y] > thr]
def outside_circle(img, bg, r_frac):
"""Count pixels that differ from the background and lie outside radius r_frac x width of the centre."""
w, h = img.size
cx, cy, r = (w - 1) / 2.0, (h - 1) / 2.0, r_frac * w
rgb = img.convert("RGB").load()
n = 0
for y in range(h):
dy2 = (y - cy) ** 2
for x in range(w):
p = rgb[x, y]
if max(abs(p[0] - bg[0]), abs(p[1] - bg[1]), abs(p[2] - bg[2])) > 24 and (x - cx) ** 2 + dy2 > r * r:
n += 1
return n
def today_shape():
"""The Page today: logo.png at full size, unscaled, centred on a white square of its own width."""
src = Image.open(LOGO_PNG).convert("RGBA")
w, h = src.size
sq = Image.new("RGBA", (w, w), WHITE)
sq.alpha_composite(src, (0, (w - h) // 2))
return sq
def profile_width():
"""The square size at which logo.png fits radius LOGO_R x width UNSCALED (never upsampled), at least 720."""
src = Image.open(LOGO_PNG).convert("RGBA")
pts = alpha_points(src)
xs, ys = [p[0] for p in pts], [p[1] for p in pts]
cx, cy = (min(xs) + max(xs)) / 2.0, (min(ys) + max(ys)) / 2.0
dmax = max(math.hypot(x - cx, y - cy) for x, y in pts) + 1
return max(720, int(math.ceil(dmax / LOGO_R / 2.0)) * 2), (cx, cy)
def build_profile(bg, name):
w, (cx, cy) = profile_width()
logo = Image.open(LOGO_PNG).convert("RGBA")
img = Image.new("RGBA", (w, w), bg)
img.alpha_composite(logo, (int(round(w / 2.0 - cx)), int(round(w / 2.0 - cy))))
path = os.path.join(OUT, name)
img.convert("RGB").save(path, optimize=True)
return path
# ------------------------------------------------------------------ covers
def background(glow_at=(0.78, 0.25)):
"""bg-0 with the site's 50 px grid (3 % blue, index.html) and the og-image's soft blue glow; top blue rule."""
img = Image.new("RGBA", (CW, CH), BG0)
glow = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
gd = ImageDraw.Draw(glow)
gx, gy, gr = glow_at[0] * CW, glow_at[1] * CH, 520
gd.ellipse((gx - gr, gy - gr, gx + gr, gy + gr), fill=(0, 131, 216, 46))
img.alpha_composite(glow.filter(ImageFilter.GaussianBlur(180)))
grid = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
d = ImageDraw.Draw(grid)
step = int(50 * SCALE)
for x in range(0, CW, step):
d.line((x, 0, x, CH), fill=(0, 131, 216, 9), width=2)
for y in range(0, CH, step):
d.line((0, y, CW, y), fill=(0, 131, 216, 9), width=2)
img.alpha_composite(grid)
ImageDraw.Draw(img).rectangle((0, 0, CW, 8), fill=BLUE)
return img
def content_a():
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
mid = (SAFE[0] + SAFE[2]) / 2.0
size = fit_size(SLOGAN, "ExtraBold", (SAFE[2] - SAFE[0]) - 120, 66)
w1 = text_width(L1 + " ", size, "ExtraBold")
w = text_width(SLOGAN, size, "ExtraBold")
x0 = mid - w / 2.0
draw_text(c, x0, 238, L1 + " ", size, "ExtraBold", TEXT1)
draw_text(c, x0 + w1, 238, L2, size, "ExtraBold", BLUE_BRIGHT)
draw_text(c, mid, 300, DOMAIN, 34, "SemiBold", TEXT2, align="center")
return c
def rack(d, x, y, w, h, fill, dot):
"""Three stacked server units with status dots — the logo's own motif, drawn as plain shapes."""
uh = (h - 2 * 8) / 3.0
for i in range(3):
ty = y + i * (uh + 8)
d.rectangle((x, ty, x + w, ty + uh), fill=fill)
for j in range(3):
cx = x + 20 + j * 22
d.ellipse((cx - 5, ty + uh / 2 - 5, cx + 5, ty + uh / 2 + 5), fill=dot)
def tile(d, cx, cy, s, kind):
"""A plain app tile (no third-party logo): a square with a simple generic glyph."""
x0, y0, x1, y1 = cx - s / 2, cy - s / 2, cx + s / 2, cy + s / 2
d.rectangle((x0, y0, x1, y1), fill=BG2, outline=LINE, width=3)
g = s * 0.28
if kind == "photo":
d.polygon([(cx - g, cy + g * 0.7), (cx - g * 0.2, cy - g * 0.5), (cx + g * 0.3, cy + g * 0.2),
(cx + g * 0.6, cy - g * 0.1), (cx + g, cy + g * 0.7)], fill=BLUE_BRIGHT)
d.ellipse((cx + g * 0.35, cy - g, cx + g * 0.85, cy - g * 0.5), fill=TEXT2)
elif kind == "doc":
d.rectangle((cx - g * 0.75, cy - g, cx + g * 0.75, cy + g), outline=TEXT1, width=5)
for i in range(3):
yy = cy - g * 0.45 + i * g * 0.45
d.line((cx - g * 0.45, yy, cx + g * 0.45, yy), fill=BLUE_BRIGHT, width=5)
elif kind == "play":
d.polygon([(cx - g * 0.6, cy - g), (cx + g, cy), (cx - g * 0.6, cy + g)], fill=BLUE_BRIGHT)
elif kind == "lock":
d.arc((cx - g * 0.6, cy - g * 1.2, cx + g * 0.6, cy), 180, 360, fill=TEXT1, width=6)
d.rectangle((cx - g * 0.85, cy - g * 0.6, cx + g * 0.85, cy + g * 0.9), fill=TEXT1)
d.ellipse((cx - g * 0.18, cy - g * 0.1, cx + g * 0.18, cy + g * 0.26), fill=BG2)
elif kind == "folder":
d.polygon([(cx - g, cy - g * 0.7), (cx - g * 0.2, cy - g * 0.7), (cx, cy - g * 0.4), (cx + g, cy - g * 0.4),
(cx + g, cy + g * 0.8), (cx - g, cy + g * 0.8)], fill=BLUE_BRIGHT)
def content_b():
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
d = ImageDraw.Draw(c)
tx = SAFE[0] + 24 # text stays above the profile circles (y < 316)
draw_text(c, tx, 170, L1, 64, "ExtraBold", TEXT1)
draw_text(c, tx, 244, L2, 64, "ExtraBold", BLUE_BRIGHT)
draw_text(c, tx, 290, DOMAIN, 32, "SemiBold", TEXT2)
# the home with its server, on the right
hx, hy, hw = 1010, 300, 220 # house body: left, top, width
tiles = [(975, 110, "photo"), (1140, 92, "doc"), (1260, 210, "play"), (1260, 400, "lock"), (970, 520, "folder")]
for x, y, _ in tiles:
d.line((hx + hw / 2, hy + 90, x, y), fill=(46, 168, 245, 110), width=4)
d.rectangle((hx, hy, hx + hw, hy + 190), fill=BG1, outline=BLUE_BRIGHT, width=6)
d.polygon([(hx - 30, hy + 4), (hx + hw / 2, hy - 120), (hx + hw + 30, hy + 4)], fill=BG1, outline=BLUE_BRIGHT)
d.line([(hx - 30, hy + 4), (hx + hw / 2, hy - 120), (hx + hw + 30, hy + 4)], fill=BLUE_BRIGHT, width=6, joint="curve")
rack(d, hx + 40, hy + 40, hw - 80, 110, BLUE, TEXT1)
for x, y, kind in tiles:
tile(d, x, y, 92, kind)
return c
def content_c():
c = Image.new("RGBA", (CW, CH), (0, 0, 0, 0))
d = ImageDraw.Draw(c)
tx = SAFE[0] + 24
draw_text(c, tx, 180, L1, 52, "ExtraBold", TEXT1)
draw_text(c, tx, 246, L2, 52, "ExtraBold", BLUE_BRIGHT)
draw_text(c, tx, 292, DOMAIN, 30, "SemiBold", TEXT2)
shot = Image.open(SHOT).convert("RGBA")
# The domain chip next to the title names the demo box; covered with the page colour (copy only, source unchanged).
ImageDraw.Draw(shot).rectangle((684, 32, 826, 70), fill=hexc("#0B1322"))
sw = 470
sh = int(round(sw * shot.height / shot.width))
shot = shot.resize((sw, sh), Image.LANCZOS)
fx, fy, b = 800, 110, 14 # frame left/top, bezel
d.rectangle((fx, fy, fx + sw + 2 * b, fy + sh + 2 * b), fill=BG2, outline=LINE, width=3)
c.alpha_composite(shot, (fx + b, fy + b))
base_y = fy + sh + 2 * b
d.polygon([(fx - 30, base_y + 22), (fx + sw + 2 * b + 30, base_y + 22), (fx + sw + 2 * b, base_y),
(fx, base_y)], fill=LINE)
return c
def check_safe(name, content):
pts = alpha_points(content.resize((CW // 4, CH // 4)), thr=8)
xs = [p[0] * 4 for p in pts]
ys = [p[1] * 4 for p in pts]
bbox = (min(xs), min(ys), max(xs) + 3, max(ys) + 3)
inside = bbox[0] >= SAFE[0] and bbox[1] >= SAFE[1] and bbox[2] <= SAFE[2] and bbox[3] <= SAFE[3]
check(inside, "%s: content box %s inside the safe area %s" % (name, bbox, SAFE))
hits = 0
for q in QUIET:
hits += sum(1 for x, y in zip(xs, ys) if q[0] <= x <= q[2] and q[1] <= y <= q[3])
check(hits == 0, "%s: %d content pixels where the profile circle sits (computer or phone)" % (name, hits))
def build_cover(name, content, glow):
img = background(glow)
img.alpha_composite(content)
path = os.path.join(OUT, name)
img.convert("RGB").save(path, optimize=True)
return path
# ------------------------------------------------------------------ preview
def b64(path):
with open(path, "rb") as fh:
return "data:image/png;base64," + base64.b64encode(fh.read()).decode()
def preview(profiles, covers):
dk, ph = DESK_CIRCLE, PHONE_CIRCLE
sx0, sy0, sx1, sy1 = SAFE
pct = lambda v, tot: "%.3f%%" % (100.0 * v / tot) # noqa: E731
prof_html = "".join(
'<figure><div class="row"><img class="pc" src="%s" alt=""><img class="pc sm" src="%s" alt="">'
'<img class="pc xs" src="%s" alt=""></div><figcaption>%s — 176 px, 96 px, 40 px</figcaption></figure>'
% (b64(p), b64(p), b64(p), label) for label, p in profiles)
cov = []
rec = profiles[0][1]
for label, p in covers:
src = b64(p)
safe = ('<div class="safe" style="left:%s;top:%s;width:%s;height:%s"></div>'
% (pct(sx0, CW), pct(sy0, CH), pct(sx1 - sx0, CW), pct(sy1 - sy0, CH)))
desk = ('<div class="desk"><img src="%s" alt="">%s<img class="ov" src="%s" alt="" '
'style="left:%s;top:%s;width:%s"></div>'
% (src, safe, b64(rec), pct(dk[0], CW), pct(dk[1], CH), pct(dk[2], CW)))
crop_l = PHONE_X0 / CW_PHONE * 100
phone = ('<div class="phone"><div class="crop"><img src="%s" alt="" style="width:%.3f%%;left:-%.3f%%">'
'</div><img class="ov" src="%s" alt="" style="left:%s;top:%s;width:%s"></div>'
% (src, CW / CW_PHONE * 100, crop_l, b64(rec), pct(ph[0] - PHONE_X0, CW_PHONE),
pct(ph[1], CH), pct(ph[2], CW_PHONE)))
cov.append('<section><h2>%s</h2><p class="k">Computer (820 x 312), dashed = safe area (preview only)</p>%s'
'<p class="k">Phone (640 x 360 crop)</p>%s</section>' % (label, desk, phone))
html = """<!doctype html>
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>Facebook Page pictures</title>
<style>
:root{--bg-0:#0A1220;--bg-1:#0F1B2E;--line:#22344F;--text-1:#EDF2F9;--text-2:#94A6BF;--blue:#0083D8}
body{margin:0;background:var(--bg-0);color:var(--text-1);font:15px/1.5 -apple-system,'Segoe UI',sans-serif;padding:24px 16px}
main{max-width:860px;margin:0 auto}h1{font-size:22px;margin:0 0 4px}h2{font-size:17px;margin:32px 0 8px}
.k{color:var(--text-2);margin:12px 0 6px;font-size:13px}figure{margin:16px 0}.row{display:flex;gap:16px;align-items:center;flex-wrap:wrap}
.pc{width:176px;height:176px;border-radius:50%%;border:4px solid #fff;display:block}.sm{width:96px;height:96px;border-width:3px}
.xs{width:40px;height:40px;border-width:1px}figcaption{color:var(--text-2);font-size:13px;margin-top:6px}
.desk{position:relative;max-width:820px;margin-bottom:56px}.desk>img:first-child{width:100%%;display:block}
.ov{position:absolute;border-radius:50%%;border:3px solid #fff;box-sizing:border-box}
.safe{position:absolute;border:2px dashed #E0A93E;box-sizing:border-box;pointer-events:none}
.phone{position:relative;width:min(360px,100%%);aspect-ratio:640/360;margin-bottom:48px}
.crop{position:absolute;inset:0;overflow:hidden}.crop img{position:absolute;top:0;height:100%%}
</style></head><body><main>
<h1>Felhom.eu - Facebook pictures</h1>
<p class="k">Profile picture: the DARK background is recommended. The logo's lettering is white with a thin navy edge,
so on white it fades at 40 px; on dark it reads solid, as on the website. The covers below show the dark one.</p>
<h2>Profile picture</h2>%s
%s
</main></body></html>
""" % (prof_html, "".join(cov))
path = os.path.join(OUT, "preview.html")
with open(path, "w", encoding="utf-8") as fh:
fh.write(html)
return path
# ------------------------------------------------------------------ main
def main():
os.makedirs(OUT, exist_ok=True)
print("circle check (radius %.2f x width)" % CIRCLE_R)
try:
before = today_shape()
n0 = outside_circle(before, WHITE, CIRCLE_R)
print(" today's shape (logo.png %dx%d on a %d square): %d logo pixels outside the circle — expected > 0"
% (Image.open(LOGO_PNG).width, Image.open(LOGO_PNG).height, before.width, n0))
check(n0 > 0, "the check convicts today's shape (control)")
prof = [("profile-white.png", WHITE), ("profile-dark.png", BG0)]
built = []
for name, bg in prof:
p = build_profile(bg, name)
n = outside_circle(Image.open(p), bg, CIRCLE_R)
check(n == 0, "%s: %d logo pixels outside the circle" % (name, n))
built.append(p)
print("safe-area check (computer 820x312 + phone 640x360; profile circles kept clear)")
covers = [("cover-a.png", content_a(), (0.78, 0.25)), ("cover-b.png", content_b(), (0.70, 0.45)),
("cover-c.png", content_c(), (0.66, 0.40))]
cpaths = []
for name, content, glow in covers:
check_safe(name, content)
cpaths.append(build_cover(name, content, glow))
except RuntimeError as e:
print("RENDER FAILED: %s" % e)
return 2
print("sizes read back from the files")
for p in built + cpaths:
w, h = Image.open(p).size
want = (profile_width()[0],) * 2 if "profile" in p else (CW, CH)
check((w, h) == want, "%s: %d x %d" % (os.path.basename(p), w, h))
pv = preview([("Dark background (recommended)", built[1]), ("White background", built[0])],
[("A - calm", cpaths[0]), ("B - the idea", cpaths[1]), ("C - the product", cpaths[2])])
print("preview: %s (%d KB)" % (os.path.relpath(pv, ROOT), os.path.getsize(pv) // 1024))
if FAILS:
print("FAILED: %d check(s)" % len(FAILS))
return 1
print("all checks passed")
return 0
if __name__ == "__main__":
sys.exit(main())
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

File diff suppressed because one or more lines are too long
Binary file not shown.

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB