Skip to content

feat(friendship-ended): port Friendship Ended Generator to Go - #1013

Open
Xe wants to merge 10 commits into
masterfrom
Xe/friendship-ended
Open

Xe wants to merge 10 commits into
masterfrom
Xe/friendship-ended

Conversation

@Xe

@Xe Xe commented Sep 25, 2026

Copy link
Copy Markdown
Owner

Summary

Ports the PHP "Friendship Ended Generator" by Leigh Aucoin (GPL-2.0,
https://github.com/lmaucoin/friendship-ended) to a Go binary at
cmd/friendship-ended.

  • Renders with github.com/fogleman/gg using the original layout (800x600
    new friend photo, crossed-out old friend photos, Comic Sans Bold text).
  • Stores renders and source uploads in Tigris. Result pages are unlisted, and
    images are served through presigned URL redirects.
  • Web UI is templ, styled with the xe-design-system.
  • Hardening: render concurrency capped at 2, 40 megapixel upload limit, EXIF
    auto-orientation, transparent uploads flattened onto white, printable-only
    names, server timeouts, and graceful shutdown on SIGINT/SIGTERM.
  • GPL assets (font, overlays, treasure.gif) live in cmd/friendship-ended/assets
    with their license and provenance notes.

Design spec: docs/superpowers/specs/2026-09-18-friendship-ended-design.md
Implementation plan: docs/superpowers/plans/2026-09-18-friendship-ended.md

Local development

Put AWS_PROFILE=tigris in cmd/friendship-ended/.env and run the binary from
that directory. internal.HandleStartup() loads .env from the working
directory.

Testing

  • go vet ./cmd/friendship-ended/ passes
  • go test ./cmd/friendship-ended/ passes (render, handlers, and an in-memory
    store covering upload limits and partial put failures)

Assisted-by: Claude Opus 5.5 via Claude Code

Xe added 10 commits September 18, 2026 14:06
Design for porting the PHP Friendship Ended Generator to a Go binary
using fogleman/gg for rendering and Tigris for storing renders and
source uploads.

Assisted-by: Claude Opus 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Break the friendship-ended port into renderer, handlers/storage, and
entrypoint tasks. Update the spec with details found while checking
the gg, templ, and tigris APIs.

Assisted-by: Claude Opus 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Port the ImageMagick compositing from the PHP friendship-ended
generator to fogleman/gg. Reuse the original GPL-2.0 overlay and
treasure assets and embed Comic Sans MS Bold for the text.

Assisted-by: Claude Opus 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Serve the upload form and unlisted result pages styled with the
xe-design-system. Renders and the original uploads go to Tigris, and
result images are served through presigned URL redirects.

Assisted-by: Claude Opus 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Assisted-by: Claude Opus 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Wire the renderer, Tigris store, and handlers together behind flags
for bind address, bucket, public base URL, and presign expiry.

Assisted-by: Claude Opus 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Cap decode+render+JPEG-encode work to maxConcurrentRenders (2) via a
semaphore on Server, acquired after name validation and before reading
uploads, released right after encoding (before storage puts) with the
decoded images dereferenced so memory does not stay pinned during the
puts. A client that disconnects while waiting gives up immediately via
select on the request context.

Add ReadTimeout/WriteTimeout/IdleTimeout to the http.Server and graceful
shutdown on SIGINT/SIGTERM with a 30s Shutdown deadline, treating
http.ErrServerClosed as normal.

readUpload now decodes with imaging.Decode(..., imaging.AutoOrientation
(true)) so EXIF-rotated photos come out upright instead of sideways.

validName now rejects any non-printable rune (control characters like
newlines or tabs), not just empty or over-length names.

The image handler's Cache-Control max-age is now capped at half the
presign expiry (min(300s, expiry/2)), so a cached redirect can never
outlive the presigned URL it points to; newServer takes the presign
expiry as an explicit parameter to make this possible.

memStore gains a failSuffix option so tests can fail one specific put
without failing the whole request, used to prove meta.json is still
never written when an earlier image put fails.

Assisted-by: Claude Opus 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
… mask reset

Transparent uploads (for example a PNG with an alpha channel) used to
render as a black rectangle, since the final JPEG has no alpha channel
of its own to fall back on. Add flattenWhite, which composites the
resized image over an opaque white background with image/draw before
handing it to gg, and use it for both the new-friend picture and the
two old-friend photos inside crossOut.

Also swap the hand-rolled all-0xff image.Alpha mask reset in drawLine
for gg's own dc.ResetClip(), which does the same thing (clears the
mask) without allocating or looping over pixels, and fix the textLine
doc comment, which had the pre/post-scale coordinate space backwards.

Assisted-by: Claude Opus 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
Document that AWS credentials come from the standard env vars or
AWS_PROFILE, and that local development needs AWS_PROFILE=tigris in
cmd/friendship-ended/.env with the binary run from that directory,
since internal.HandleStartup() loads .env from the working directory.

Update the design doc to match the shipped code: --bucket default is
xe-friendship-ended, the font is extracted with 7z (not cabextract),
newServer takes the presign expiry, the render semaphore bounds
concurrent renders to 2, server timeouts and graceful shutdown are in
place, uploads get EXIF auto-orientation, transparent areas flatten
onto white, and names must be printable.

Assisted-by: Claude Opus 5 via Claude Code
Signed-off-by: Xe Iaso <me@xeiaso.net>
@github-actions

Copy link
Copy Markdown
Contributor

The latest Buf updates on your PR. Results from workflow Go / build (pull_request).

BuildFormatLintBreakingUpdated (UTC)
✅ passed✅ passed✅ passed✅ passedSep 25, 2026, 9:47 PM

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant