Facebook Page: profile pictures, three covers, preview page and texts (marketing/facebook); R-916 opened
gates / gates (push) Successful in 4m24s
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:
@@ -75,7 +75,7 @@ box**.
|
||||
explicit paths only, `git pull --rebase` before every push.
|
||||
- **`REPORT.md` is overwritten, so two sessions in this repo clobber each other.** The second session
|
||||
writes **`REPORT-<topic>.md`** and never touches the shared `REPORT.md`.
|
||||
- `CHANGELOG.md` here is **per-area**: `hub/`, `scripts/`, `website/`.
|
||||
- `CHANGELOG.md` here is **per-area**: `hub/`, `scripts/`, `website/`, `marketing/`.
|
||||
|
||||
<!-- The sweep incident: v0.47.0, commit 146d165 — a red-proof-mutated guard got swept to main. -->
|
||||
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
# REPORT — Facebook Page pictures and texts (2026-10-08)
|
||||
|
||||
Own file (parallel sessions share this clone). Baseline `felhom.eu` @ `e9e6300cfc`.
|
||||
|
||||
## For the operator — your click list, in order
|
||||
|
||||
1. Open `marketing/facebook/out/preview.html` from Gitea (Download, then open the file). Pick cover A, B or C.
|
||||
2. Page → profile picture → upload `marketing/facebook/out/profile-dark.png` (recommended).
|
||||
3. Page → cover photo → upload the cover you picked.
|
||||
4. Page settings → username → set it, for example `felhom`.
|
||||
5. Page → action button → „Weboldal" (Website) → `https://felhom.eu`.
|
||||
6. Page → Intro / Bemutatkozás → paste `COPY.md` §1. About → details → paste §2.
|
||||
7. Meta Business Suite → Inbox → automations → welcome message → paste §3 and turn it on.
|
||||
8. The three posts (§4) wait until the app is Live (R-915). If you post them now, only people with a role on the app see them.
|
||||
|
||||
## Results
|
||||
|
||||
| Item | Result |
|
||||
|---|---|
|
||||
| Logo master | `website/assets/logo.png` (645 x 408). `logo.svg` refused: its lettering is live text in fonts DooPlex lacks („M+ 2c", „Vremena Grotesk") → R-916 |
|
||||
| Colours | `site.css` `:root`: bg `#0A1220`/`#0F1B2E`/`#16263F`, line `#22344F`, text `#EDF2F9`/`#94A6BF`, blue `#0083D8`/`#2EA8F5`. The site is dark-only (one token set) |
|
||||
| Font | Plus Jakarta Sans from `website/assets/fonts/*.woff2` (ExtraBold, SemiBold) |
|
||||
| Renderer | Pillow 11.1.0. librsvg was tried and dropped: pango fell back to DejaVu for woff2, and the SVG logo draws the wrong letters |
|
||||
| Circle check | today's shape (logo.png on a 645 square): **6823** pixels outside radius 0.42 x width. New pictures: **0** and **0** |
|
||||
| Safe area | covers B and C **failed first** (51 content pixels in the phone profile circle's corner; the frame past the phone crop); moved; all three now 0 and inside (306, 40, 1334, 584) |
|
||||
| Sizes read from the files | profile-dark 932 x 932, profile-white 932 x 932, cover-a/b/c 1640 x 624 each |
|
||||
| Profile background | dark recommended: the lettering is white with a thin navy edge and fades on white at 40 px (by eye, 176 + 40 px mock) |
|
||||
| Copy | intro 99 of 100 characters; no price; „kérjük" absent (ASCII fragment `kerjuk` + accented form; controls: `szerver` found, `xyzzy` not); links `/alkalmazasok`, `/kapcsolat`, `/` answer 200 |
|
||||
|
||||
**Sizes (READ, second-hand — Meta's help page needs a browser; Claude in Chrome was not available: no
|
||||
`claude-in-chrome` tools in this session (searched by name and keyword), and the workspace notes say it does not run on DooPlex):**
|
||||
cover 820 x 312 computer, 640 x 360 phone; profile ~176 px; profile circle at 16/176 px (computer), 24/196 px
|
||||
(phone) — postfa.st/sizes/facebook/cover, linearity.io/templates/facebook-size-guide, fasturtle.com.
|
||||
**First-hand (Meta developer docs):** Page `about` 100 characters; `POST /{page}/picture` needs the `MANAGE` task;
|
||||
the cover needs `EDIT_PROFILE` + `business_management` (developers.facebook.com/docs/graph-api/reference/page/, …/page/picture).
|
||||
|
||||
**Gitea paths:** `https://gitea.dooplex.hu/admin/felhom.eu/src/branch/main/marketing/facebook/out/` — `preview.html`,
|
||||
`profile-dark.png`, `profile-white.png`, `cover-a.png`, `cover-b.png`, `cover-c.png`.
|
||||
|
||||
**Register:** 138 → 139. Opened R-916 (logo vector master, operator). Edited R-914 (scheduled-post listing as the removal
|
||||
proof; the picture API's tasks), R-915 (footer link after Live). Closed 0. The brief expected 0 opened: R-916 is a gap I
|
||||
cannot fix here (it needs the fonts), so by the size rule it is a row.
|
||||
|
||||
**Teardown:** no Graph API call, no key read. Host: nothing provisioned. Hub: nothing. Facebook: nothing.
|
||||
|
||||
## Fixed without a row
|
||||
|
||||
- Covers B and C, caught by the build's own safe-area check, moved before the first commit.
|
||||
- `CLAUDE.md` line 78: „per-area: `hub/`, `scripts/`, `website/`" → adds `marketing/` (a new area with its own CHANGELOG).
|
||||
|
||||
## Observations
|
||||
|
||||
- The Planner screenshot (operator, 2026-10-08) shows nothing on 15 October, the test posts' date — a different-channel
|
||||
look at the spike's removals — NOT-A-FINDING: recorded in R-914, which still asks for the listing proof.
|
||||
- The dashboard screenshots used on the website show the demo box's domain; cover C covers it in its copy —
|
||||
NOT-A-FINDING: already public on the website, covered anyway.
|
||||
- `logo.svg` lettering — FILED: R-916.
|
||||
@@ -3,15 +3,18 @@
|
||||
**Ready for the first real tester (Tester-2): yes. Tester 2 (a laptop) is off; nothing was sent to it.**
|
||||
|
||||
**Updated 2026-10-08 (evening): hub 0.143.1; demo-hp, demo-felhom and Tester 1 run agent 0.153.0 and controller
|
||||
0.303.0 (nothing delivered today — tonight is the second kernel night). The open-items list is at 138. Reports:
|
||||
0.303.0 (nothing delivered today — tonight is the second kernel night). The open-items list is at 139. Reports:
|
||||
`REPORT-day-2026-10-08.md` (morning), `REPORT-day2-2026-10-08.md` (afternoon), `REPORT-day3-2026-10-08.md` (evening).**
|
||||
|
||||
## Facebook Page (2026-10-08): Claude can post — the public cannot see it yet
|
||||
## Facebook Page (2026-10-08): Claude can post; the pictures and texts are ready for you
|
||||
|
||||
- The robot key works. It never runs out. After your Page click, it reaches the Felhom.eu Page.
|
||||
- Test: one scheduled text post and one scheduled photo. Accents came back exact. Both deleted, and proven gone.
|
||||
- The robot key works and reaches the Page. A test post and a test photo were made, checked and deleted.
|
||||
- **New pictures and texts (needs you, about 10 minutes):** open the preview page, pick one of three covers, upload
|
||||
the profile picture and the cover by hand, paste the texts. The list is in the pictures report.
|
||||
- **Later, before real posts:** switch the Meta app to „Live". It needs a Terms of Service web address (the
|
||||
legal pages item). If you do nothing: posts are seen only by people with a role on the app.
|
||||
- **When you have time:** the logo needs a clean vector copy, made on the machine that has its fonts. If you do
|
||||
nothing: the pictures stay as they are; large prints of the logo will be soft.
|
||||
|
||||
## Evening (2026-10-08): your ten answers (D1–D10) built — they ship tomorrow
|
||||
|
||||
|
||||
@@ -263,7 +263,7 @@ stopping line that lies.
|
||||
| **R-814** | Hub & operator | P4 | `PBS-storage-1` (u629193, box 611421) still `status=active`, 19.9 MB | **VERIFY** (2026-10-03 triage: a July watch row with no id; given R-814. WAITING-ON-OPERATOR — no record found that the box was deleted.) — WAITING-ON-OPERATOR | operator console | Delete the box | operator |
|
||||
| **R-844** | Hub & operator | P4 | **The household's OS-update line exists only on the hub's customer timeline.** 2026-10-04: the box itself has no event surface for agent results (the controller UI shows no timeline), so `os_update_applied` is a hub customer event (info: recorded, never mailed). Its stored text is the hub's English sentence; the hu/en bundle text (`mail.event.os_update_applied`) is used only if it is ever mailed. Fix direction: a controller-side line (the controller already polls the agent's local API) when the box gets a household timeline. `audits/os-guest-lane-2026-10-04/partG/hub-customer-timeline-demo-hp.txt` | **READY — owner: CC** **2026-10-05 (burn-down night): NEEDS A DESIGN** — a household timeline on the box does not exist yet. | — | — | CC |
|
||||
|
||||
## Business & legal — 8 rows (P2 4, P4 4)
|
||||
## Business & legal — 9 rows (P2 4, P4 5)
|
||||
|
||||
| ID | Category | Sev | What | State | Blocked on | Next action | Owner |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
@@ -274,8 +274,9 @@ stopping line that lies.
|
||||
| **R-901** | Business & legal | P2 | **Two kinds of a household's data outlive the deletion of the customer, and no document says when they go.** FOUND 2026-10-08 (R-813 drafting), read in source: the hub keeps `events` and `notification_log` on purpose after a customer is deleted (`hub/internal/store/customer_delete.go:24-26`, „the audit trail outlives every lifecycle tier") and nothing prunes `notification_log`; DooPlex's copy of ep0 (`ep0-copy`) pulls with `remove-vanished false` and prunes only to keep-weekly 8 (`runbooks/ep0-datastore-copy.md`), so a deleted customer's last 8 weekly encrypted whole-guest copies stay on DooPlex with no end date. A privacy notice cannot promise deletion until this is decided. **-- 2026-10-08 14:16 operator ruling D9 (`09` §3 decision 193):** yes — CC installs the DooPlex job, dry run first, then daily, after the 2026-10-09 releases are read back. | **READY — ruled 2026-10-08 09:04 (`09` §3 decision 181): audit rows (`events`, `notification_log`) of a deleted customer kept 1 year, then deleted; the customer's `ep0-copy` namespace removed within 30 days; both go into the privacy-notice draft.** Close only when both are live. | R-813 | Build the hub's daily deletion (ships with the next hub release) and the DooPlex `ep0-copy` removal job (written; run needs the operator's word); write both times into the privacy-notice draft | CC |
|
||||
| **R-89** | Business & legal | P4 | Retention as a per-customer **commercial** policy on the hub | READY (increment 2) | — | Policy object + reconciler → ep0 prune job; keep box tokens write-only | CC |
|
||||
| **R-794** | Business & legal | P4 | **[P3-LOW] redis 7.4 (RSALv2 / SSPL, not OSI) runs as a private cache in seven apps: dawarich, docmost, immich, nextcloud, outline, paperless-ngx, romm.** READ 2026-10-02 (`audits/licences-2026-10-02/TABLE.md`). Read as permitted (a private cache only its app uses is not Redis offered as a service — inferred). Valkey (BSD-3) or redis 8 (AGPL option) removes the question. **Needs:** a ladder step per app to valkey or redis 8, through the harness — no hurry. | **READY — rank P3-LOW; owner: CC** **Re-ranked 2026-10-03: P3→P4: the row itself says no hurry; usage read as permitted.** | — | — | CC |
|
||||
| **R-914** | Business & legal | P4 | **Write the Felhom Facebook Page skill from the spike's findings.** Spike 2026-10-08 (`audits/SPIKE-facebook-page-api-2026-10-08.md`): key valid, never expires; the Page (`1360018983863273`) is reached with CREATE_CONTENT/MODERATE/ANALYZE; a scheduled text post and a scheduled photo were created, read back byte-equal and deleted (removal proven). Probe `scripts/facebook/fb_probe.py`. Gap to close in the skill: a scheduled photo's publish state was not read (no `post_id` returned). | **READY — owner: CC** | — | Write the skill (drafts scheduled for operator review by default); read a scheduled photo back through the Page's scheduled-post listing | CC |
|
||||
| **R-915** | Business & legal | P4 | **The Meta app `felhom.eu` is in development mode, so posts it makes are seen only by people with a role on the app.** READ 2026-10-08 (Meta docs, cited in the spike); not measured. MEASURED: development mode does not refuse posting (both test writes HTTP 200). Live needs display name, contact e-mail, a Terms of Service URL, an app icon, a category and the app purpose (privacy-policy and data-deletion URLs listed beside them). The robot's Page assignment, missing at first, was done by the operator the same day. | **WAITING-ON-OPERATOR** | R-813 (the Terms of Service URL) | Before real public posts: switch the app to Live in the Meta developer page. If nothing is done: posts stay invisible to the public | operator |
|
||||
| **R-914** | Business & legal | P4 | **Write the Felhom Facebook Page skill from the spike's findings.** Spike 2026-10-08 (`audits/SPIKE-facebook-page-api-2026-10-08.md`): key valid, never expires; the Page (`1360018983863273`) is reached with CREATE_CONTENT/MODERATE/ANALYZE; a scheduled text post and a scheduled photo were created, read back byte-equal and deleted (removal proven). Probe `scripts/facebook/fb_probe.py`. Gap to close in the skill: a scheduled photo's publish state was not read (no `post_id` returned). **2026-10-08 (Page pictures task) — two more for the skill:** (1) **the removal proof was not a proof**: the text post's after-DELETE answer was (#10) „does not exist, cannot be loaded due to missing permission…" — that message also means a permission gap. The skill proves removal by listing the Page's scheduled posts before and after the delete (present, then absent); the operator's Planner view on 2026-10-08 showed nothing on 15 October (a different channel, by eye). (2) **pictures by API** (READ, developers.facebook.com/docs/graph-api/reference/page/picture and …/reference/page/): `POST /{page}/picture` needs the `MANAGE` task, which the robot does not have (measured task list); the cover is `POST /{page}` `cover=<photo id>`, „only by the Page Admin or Page Editor with `EDIT_PROFILE`" + `business_management`. Neither names `pages_manage_metadata`. Today the pictures are uploaded by hand (`marketing/facebook/README.md`). | **READY — owner: CC** | — | Write the skill (drafts scheduled for operator review by default); read a scheduled photo back through the Page's scheduled-post listing | CC |
|
||||
| **R-915** | Business & legal | P4 | **The Meta app `felhom.eu` is in development mode, so posts it makes are seen only by people with a role on the app.** READ 2026-10-08 (Meta docs, cited in the spike); not measured. MEASURED: development mode does not refuse posting (both test writes HTTP 200). Live needs display name, contact e-mail, a Terms of Service URL, an app icon, a category and the app purpose (privacy-policy and data-deletion URLs listed beside them). The robot's Page assignment, missing at first, was done by the operator the same day. | **WAITING-ON-OPERATOR** | R-813 (the Terms of Service URL) | Before real public posts: switch the app to Live in the Meta developer page. If nothing is done: posts stay invisible to the public. After Live: CC adds the Page link to the website footer (not before — a link to a Page nobody can read is worse than none) | operator |
|
||||
| **R-916** | Business & legal | P4 | **The logo has no usable vector master: `website/assets/logo.svg` sets „felhom.eu" as live text in the fonts „M+ 2c" and „Vremena Grotesk", which DooPlex does not have, so every renderer here draws other letters.** SEEN 2026-10-08 (Facebook pictures task): librsvg drew the lettering in DejaVu; `fc-match` resolves the family to DejaVu Sans. The PNG (645 x 408) is the only faithful copy, which caps every picture made from it at about that size (`marketing/facebook/README.md`). | **WAITING-ON-OPERATOR** | the machine with the fonts | In Inkscape on that machine: select the lettering → Path → Object to Path → save as `website/assets/logo-master.svg`; then `marketing/facebook/build.py` can use it. If nothing is done: the PNG stays the master; larger prints will be soft | operator |
|
||||
|
||||
## Process & tooling — 23 rows (P3 3, P4 20)
|
||||
|
||||
|
||||
@@ -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).
|
||||
@@ -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
|
||||
```
|
||||
@@ -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.
|
||||
@@ -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 |
Reference in New Issue
Block a user