Documentation
¶
Overview ¶
Package imageedit is the GoFastr image crop / annotate / redact plugin. It applies the pdf plugin's design — the cage is the product, not the tax — to a second file format, and in doing so proves that shape was general rather than a one-off.
Bytes arrive over the postMessage bridge: the host fetches the source image (GET /img/{id}, session + CSRF attached) and pushes it into the frame; the frame runs under connect-src 'none' and fetches nothing. A frame holding a confidential screenshot therefore cannot exfiltrate it.
The canonical doc (schema imageedit-v1) is an OPERATION LIST, never pixels: {src, crop, rotate, annotations[], redactions[]} in source-image coordinates. The frame previews by applying that list to a canvas; the SERVER re-renders the same list with the standard library's image packages for every export, strips EXIF by full re-encode, enforces size and dimension caps, and verifies redactions against the produced bytes before releasing them. A client that lies about what it did cannot change what gets stored — the stored bytes are a function of the doc, rendered by Go.
Operation order is FIXED and documented (docs/imageedit.md): crop → rotate → annotate → redact. Crop then rotate is not the same picture as rotate then crop; pinning the order (and having both renderers implement the exact same integer pipeline) is what keeps the preview and the server output in agreement.
Capabilities: document:read, document:write, theme:read are always granted; upload:images is optional, appended exactly when the host wires WithUploadHandler. pluginhost.Allow is a capability gate, NOT authentication: it passes for anonymous callers, so any route that WRITES (POST /save, POST /export, POST /upload) must be documented as requiring the host to check the session in its own handler. See docs/imageedit.md.
Index ¶
- Constants
- Variables
- func DefaultCapabilities() []string
- func Mount(cfg MountConfig) render.HTML
- func SampleImage() []byte
- func TextWidth(s string, scale int) int
- func UIHostOption() uihost.Option
- func ValidateDoc(doc Doc) error
- type Annotation
- type ComposeResult
- type Doc
- type ExportRequest
- type MountConfig
- type Option
- func WithCapabilities(caps ...string) Option
- func WithDemoPage() Option
- func WithDemoRoute(path string) Option
- func WithDevGrantAll() Option
- func WithExportHandler(fn func(ctx context.Context, req ExportRequest) (string, error)) Option
- func WithJPEGQuality(q int) Option
- func WithMaxBytes(n int64) Option
- func WithMaxDim(n int) Option
- func WithMaxPixels(n int) Option
- func WithSaveHandler(fn func(ctx context.Context, req SaveRequest) error) Option
- func WithSource(fn func(ctx context.Context, id string) ([]byte, error)) Option
- func WithUploadHandler(fn func(ctx context.Context, req UploadRequest) (string, error)) Option
- type Plugin
- type Rect
- type Redaction
- type RenderOutput
- type SaveRequest
- type SrcRef
- type UploadRequest
- type VerifyReport
Constants ¶
const ( Name = "imageedit" Version = "0.1.0" RoutePrefix = "/__gofastr/plugin/imageedit" EditorHTMLURL = RoutePrefix + "/editor.html" EditorJSURL = RoutePrefix + "/editor.js" EditorCSSURL = RoutePrefix + "/editor.css" AdapterScriptURL = RoutePrefix + "/adapter.js" ConfigScriptURL = RoutePrefix + "/config.js" ImageRoute = RoutePrefix + "/img/{id}" UploadURL = RoutePrefix + "/upload" SaveURL = RoutePrefix + "/save" ExportURL = RoutePrefix + "/export" DemoURL = "/imageedit" SchemaVersion = "imageedit-v1" // CapUploadImages gates POST /upload (a new source image entering the // plugin). It is OPTIONAL — it is NOT in [DefaultCapabilities]. // [WithUploadHandler] appends it (the pdf WithExportHandler pattern): // reading bytes into the host's storage is ingress the host explicitly // turned on. CapUploadImages = "upload:images" )
Identity and route constants. Both this plugin and host/adapter.js hard-code these exactly (protocol-v1.md §2/§10). The demo lives at /imageedit.
These mirror the imageedit row in plugins.json; internal/registry tests pin Name + RoutePrefix against that row, so they MUST NOT drift.
Variables ¶
var ( // ErrTooLarge: bytes or declared dimensions exceed a host ceiling. ErrTooLarge = errors.New("image exceeds a size or dimension cap") // ErrUnsupportedFormat: not a decodable png/jpeg. ErrUnsupportedFormat = errors.New("unsupported image format (want png or jpeg)") // ErrBadDoc: the operation list is structurally invalid. ErrBadDoc = errors.New("invalid imageedit-v1 doc") // ErrCropOutside: the crop rect does not intersect the image. ErrCropOutside = errors.New("crop rect outside the image") // ErrSrcMismatch: doc.src.sha256 does not match the resolved bytes. ErrSrcMismatch = errors.New("source image digest mismatch") // ErrRedactionLeak: verification found redacted content in the output. ErrRedactionLeak = errors.New("redaction verification failed") // ErrConflict: a save/export handler rejected the write as stale. ErrConflict = errors.New("conflicting revision") )
Sentinel errors the handlers map onto HTTP status + code. They are the plugin's whole failure vocabulary for the render path.
Functions ¶
func DefaultCapabilities ¶
func DefaultCapabilities() []string
DefaultCapabilities is the always-on grant set advertised to the frame: reading the source image, persisting the operation list, and bridging theme tokens. upload:images is deliberately absent — it is an optionalCapability, appended by WithUploadHandler.
func Mount ¶
func Mount(cfg MountConfig) render.HTML
Mount renders the generic mount marker plus the hidden input the adapter syncs on docChanged. Drop it into a form. All interpolated values are HTML-escaped inside pluginhost.MountMarker.
func SampleImage ¶
func SampleImage() []byte
SampleImage returns the generated demo image as PNG bytes. The result is a cached copy; callers must not mutate it.
func TextWidth ¶
TextWidth is the advance width of s at scale in pixels (exported for the frame's hit-testing parity and for tests).
func UIHostOption ¶
UIHostOption injects the platform broker, this plugin's config script, and this plugin's adapter (in that order — the adapter reads the config global the config script publishes, and registers with the broker the former defines).
func ValidateDoc ¶
ValidateDoc checks an operation list against the structural bounds. The same rules run in /save (before persisting) and /export (before rendering) so the two routes cannot disagree about what a doc is. nil crop, rotate 0 and empty lists are all valid.
Types ¶
type Annotation ¶
type Annotation struct {
ID string `json:"id"`
Type string `json:"type"`
Color string `json:"color"`
Width int `json:"width"`
X int `json:"x"`
Y int `json:"y"`
W int `json:"w"`
H int `json:"h"`
X2 int `json:"x2"`
Y2 int `json:"y2"`
Size int `json:"size"`
Text string `json:"text"`
}
Annotation is one non-destructive marking drawn above the (cropped, rotated) image. Geometry is authored in SOURCE-image pixels and mapped forward through crop+rotate at render time, so annotations stay pinned to image content across later crop/rotate edits.
Type is "rect" (stroked rectangle, X/Y/W/H), "arrow" (X,Y tail → X2,Y2 head) or "text" (X,Y top-left anchor, Size = glyph cell scale). Color is #RRGGBB — a content color drawn into the image, not a theme token. Width is the stroke thickness in output pixels (≥1).
type ComposeResult ¶
type ComposeResult struct {
Out *image.NRGBA
Pre *image.NRGBA
RedactionRects []Rect // output-space, one per doc redaction (same order)
Width, Height int
}
ComposeResult is everything the export path needs after rendering: the final image, the pre-redaction composite (annotations applied, redactions not — the verifier's "what was there" reference), and the output-space redaction rects.
type Doc ¶
type Doc struct {
SchemaVersion string `json:"schemaVersion"`
Src SrcRef `json:"src"`
Crop *Rect `json:"crop,omitempty"` // nil = uncropped
Rotate int `json:"rotate"` // 0 | 90 | 180 | 270, clockwise
Annotations []Annotation `json:"annotations"`
Redactions []Redaction `json:"redactions"`
Rev int `json:"rev"`
}
Doc is the canonical imageedit-v1 operation list. It round-trips through the hidden form field like every other plugin's doc; pixels never do.
type ExportRequest ¶
type ExportRequest struct {
DocID string
Doc Doc
Bytes []byte
Format string // "png" | "jpeg"
Width int
Height int
SHA256 string // hex digest of Bytes
Report VerifyReport
}
ExportRequest is handed to the host's export handler: the authoritative re-rendered bytes (Go composed them from the operation list, stripped EXIF and verified the redactions before encoding), plus the render facts and the verification report. The handler stores them and returns a URL.
type MountConfig ¶
type MountConfig struct {
// DocID is the persistence key for the operation-list doc.
DocID string
// Doc is the initial doc JSON server-rendered into the marker
// (data-fui-plugin-doc) — the frame reads crop/rotate/annotations/
// redactions out of it on init and requests the image it names.
Doc string
// Field is the hidden-input name the adapter mirrors the current doc
// into on docChanged, so a normal form POST round-trips it. Defaults to
// "imageedit_doc". The name is published on the marker as
// data-fui-plugin-field, which is how the adapter finds it — the input
// itself is rendered after the marker, not inside it.
Field string
// MinHeight is the initial iframe height before the editor sizes itself.
MinHeight string
// Capabilities is an optional CSV grant override.
Capabilities string
}
MountConfig configures Mount.
type Option ¶
type Option func(*Plugin)
Option configures a Plugin.
func WithCapabilities ¶
WithCapabilities overrides the grant set advertised to the frame. Default: DefaultCapabilities. upload:images is appended separately by WithUploadHandler even when this fully replaces the set — the gate is on ingress the host explicitly enabled, so silently dropping it would just break loading with a 403.
Grants are matched with the framework's scope grammar at runtime, so wildcards are legal here: "upload:*" and "*:*" imply upload:images. New requires the matching handler for anything the grant set implies — a wildcard that implies an optional capability without its handler is a construction panic, exactly like the literal capability (see [Plugin.grantsCapability]).
func WithDemoPage ¶
func WithDemoPage() Option
WithDemoPage registers the self-contained themed demo page at DemoURL.
func WithDemoRoute ¶
WithDemoRoute overrides where WithDemoPage mounts the demo (default DemoURL, "/imageedit").
func WithDevGrantAll ¶
func WithDevGrantAll() Option
WithDevGrantAll short-circuits the capability gate (demo / tests). It bypasses BOTH gate sides, so the write routes behind it must still fail closed on an unwired handler (a clear error, never a panic) — see handlers.go.
func WithExportHandler ¶
WithExportHandler overrides the export hook behind POST /export. The plugin re-renders the doc server-side (render.go), verifies the redactions against the produced bytes, and hands the handler the result; the handler stores it and returns a URL. The default handler echoes a data: URL — enough to prove the round trip; a production host points this at its file store.
func WithJPEGQuality ¶
WithJPEGQuality sets the re-encode quality for JPEG sources (1–95). New panics outside 1..95 because a zero quality silently produces a mud brick and a >95 file bloats for no visible gain.
func WithMaxBytes ¶
WithMaxBytes sets the host-enforced ceiling on image bytes in transit (source, upload and produced output). Checked before decode/buffer.
func WithMaxDim ¶
WithMaxDim sets the per-axis dimension ceiling, checked at the same header-read stage as WithMaxPixels.
func WithMaxPixels ¶
WithMaxPixels sets the decode budget: images whose header declares more pixels than this are refused (413) BEFORE image.Decode allocates them.
func WithSaveHandler ¶
func WithSaveHandler(fn func(ctx context.Context, req SaveRequest) error) Option
WithSaveHandler overrides the operation-list persistence hook. The default stores the canonical doc JSON in an in-memory map keyed by DocID.
func WithSource ¶
WithSource installs the image resolver backing GET /img/{id}. The default serves SampleImage; a production host points this at its media store. The function runs in the host page with the session and CSRF token attached — the frame cannot call /img/{id} (connect-src 'none'), so authorization stays here at the data layer. Returning (nil, nil) means "no such image" (404).
func WithUploadHandler ¶
WithUploadHandler installs the ingress hook behind POST /upload AND opts the plugin into the optional upload:images capability (appended to the grant set if not already present). The handler is the host's chance to authorize the upload against the real session: pluginhost.Allow is a capability gate, not authentication, and it passes for anonymous callers.
type Plugin ¶
type Plugin struct {
// contains filtered or unexported fields
}
Plugin is the image editor. It implements framework.Plugin and mirrors the pdf/datagrid shape: opaque-origin sandboxed iframe, protocol v1 over postMessage, go:embed'd frame bundle, capability gate, host-side RPC routes. The difference is where the pixels come from: the server renders them from the doc, so the doc is the only thing that crosses.
func New ¶
New constructs a Plugin. All fail-loud validation runs here so a misconfiguration aborts construction rather than mounting a silent hole.
func (*Plugin) Capabilities ¶
Capabilities returns the grant set this plugin advertises to the frame.
func (*Plugin) LoadDoc ¶
LoadDoc returns the last-saved operation-list JSON for docID (demo round-trip). ok is false when the doc has never been saved.
func (*Plugin) Manifest ¶
func (p *Plugin) Manifest() pluginhost.Manifest
type Rect ¶
Rect is an axis-aligned integer rectangle in source-image pixels: inclusive origin (X, Y), exclusive extent (X+W, Y+H). 90° rotations keep rects axis-aligned, so the whole geometry vocabulary survives rotation.
func SampleTokenRect ¶
func SampleTokenRect() Rect
SampleTokenRect returns the source-image rect covering the sample's visible secret token. The redaction demo, the e2e journey and the Go tests all aim here.
type Redaction ¶
Redaction is one destructive removal: the region is FILLED in the final output (default black), never covered by a drawable object. Fill is #RRGGBB.
type RenderOutput ¶
type RenderOutput struct {
Bytes []byte
Format string
Width int
Height int
SHA256 string
Report VerifyReport
}
RenderOutput is the finished export artifact plus its verification.
type SaveRequest ¶
type SaveRequest struct {
DocID string
Doc Doc
DocJSON string // raw canonical JSON, verbatim (the authoritative record)
SchemaVersion string
Rev int
}
SaveRequest is the operation-list persist signal (POST /save).
type SrcRef ¶
type SrcRef struct {
Kind string `json:"kind"`
Ref string `json:"ref"`
SHA256 string `json:"sha256,omitempty"`
}
SrcRef identifies the image an operation list applies to. Kind is always "id": the host resolves Ref through WithSource. SHA256 optionally binds the doc to the exact source bytes (hex, lowercase); on mismatch the export is refused rather than applying coordinates to a different picture.
type UploadRequest ¶
UploadRequest is handed to the host's upload handler: the raw image bytes the frame read from a local file (the frame's file picker is the only thing the sandbox grants it; the bytes still cannot LEAVE except over the bridge), their sniffed format and header dimensions. The handler stores them and returns the id the doc's Src.Ref will reference.
type VerifyReport ¶
type VerifyReport struct {
RedactionsChecked int `json:"redactionsChecked"`
Failed []string `json:"failed,omitempty"` // ids whose region still leaks
Vacuous []string `json:"vacuous,omitempty"` // ids over content already == fill
EXIFStripped bool `json:"exifStripped"` // output bytes carry no EXIF
DimensionsMatch bool `json:"dimensionsMatch"` // composed dims == encoded dims
PixelsSampled int `json:"pixelsSampled"` // pixels the fill check walked
Pass bool `json:"pass"`
}
VerifyReport is the bounded audit record that travels with every export. Verdicts plus per-redaction ids, never an unbounded pixel dump.