Content: let an image field store a small image inline as base64, as an opt-in alternative to a stored file
- Dominant language
- C#
- Stars
- 6
- Forks
- 7
- Avg merge
- 4h 42m
- Merged PRs (30d)
- 307
Description
Some sites want an image kept inside the entry itself, most often a small icon, logo or avatar, rather than as a separate stored file. The image field planned in #668 stores a file id. This adds a second, opt-in way for that field to hold its value.
## Two different meanings of "saved in the database"
1. **A stored file in Postgres.** The Files module already does this by default: bytes in `StoredFileBlob`, one row per file, referenced by id. Entries stay small, image variants work, usage tracking works, and the same file can be used by many entries. This is the recommended way to keep images in the database, and #668 covers it.
2. **Inline in the entry.** The image travels as a base64 data URI inside the entry's JSON. This issue is about this one.
Inline is costly in ways that are easy to miss, so it is off by default and bounded:
- Content is event sourced. Every saved version stores the whole entry, so a 200 KB inline image saved ten times is 2 MB of history.
- Every list response, export bundle (Portability), cache entry and webhook payload that carries the entry carries the bytes.
- No image variants, no CDN, no usage tracking, and no reuse across entries.
## Proposal
On the `file` field from #668 (call it `image` when restricted to images), a field option `storage`:
- `file` (default): the value is a stored file id, as #668 specifies.
- `inline`: the value is a data URI, `data:image/png;base64,...`.
Rules for `inline`:
- **Types:** PNG, JPEG, WebP and GIF only, checked from the decoded bytes' signature, not the declared MIME type. SVG is refused, since an inline SVG can carry script.
- **Size:** a per-field `maxInlineBytes` on the decoded bytes, default 64 KB, with a server-wide hard cap (for example 512 KB) the field option cannot exceed. Refused with a 400 naming the field and the limit.
- **Dimensions:** read from the image header and refused over a pixel limit, the same guard image variants uses (`MaxSourcePixels`).
- **Delivery:** the same object shape as a stored file, so a consumer does not branch: `{ "url": "data:image/png;base64,...", "alt": "...", "width": 32, "height": 32, "inline": true }`. `alt` is a sibling property in the stored value.
- **Switching a field** from `file` to `inline` or back does not rewrite existing entries. Existing values stay valid under the mode they were saved with, and the editor offers a convert action per entry.
- **List endpoints** keep returning the inline value. A later `fields=` projection can leave it out, which is out of scope here.
## Done when
- Validation tests: a valid PNG under the limit is accepted; the same bytes declared as `image/svg+xml`, an SVG, a file over `maxInlineBytes`, a value over the hard cap, a malformed data URI, and bytes whose signature does not match the declared type are each refused with a 400 naming the field.
- A delivery test shows an inline field and a stored-file field returning the same shape.
- The field type docs state the costs above and point to Postgres file storage as the usual way to keep images in the database.
Depends on #668.
Contributor guide
Research direction
Start by reading the image-field design in #668 and trace the field's validation and delivery paths. Add the opt-in inline behavior only after understanding that dependency, then cover the listed validation and delivery tests and update the field type documentation with the stated costs and Postgres recommendation.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- csharp, postgresql
- Domain
- api, backend, databases
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100