Files
OmaPhoto/docs/project-format.md
T

12 KiB
Raw Blame History

OmaPhoto project format, versions 1–11

OmaPhoto reads and writes Compositor's project format; this page follows Compositor's own description.

A .comp project is a folder (a document package on macOS) containing manifest.json and an images/ directory of <layer UUID>.png assets.

The manifest identifies com.compositor.project, version 11 for new saves (versions 1–10 remain readable), and the sRGB working space. It stores document UUID, pixel dimensions, active layer UUID, and layers in bottom-to-top order. Each layer stores its UUID, name, visibility, transform (origin, size, clockwise rotation, flips, sampling), and optional image filename. Blank layers have no image asset.

Embedded PNGs preserve source pixels and transparency; transforms remain separate. Projects survive moving or deleting imported source photos. Saving writes a hidden sibling folder and syncs it. A first save renames it into place; a later save swaps it with the old project atomically (renameat2 with RENAME_EXCHANGE). Where the file system knows no exchange, the old project is moved aside and the new one moved in, two steps with a moment between them and a warning in the log, and the old copy is kept until the new one is synced. Unsupported versions, invalid metadata, missing assets, unsafe paths, and oversized data are rejected before replacing the live document.

Limits: 30,000 pixels per canvas/image side, a pixel budget for layer images and another as large for masks (a sixteenth of the computer's memory in pixels, between 200 and 800 million: src/Document/DocumentLimits.cpp), 200 million pixels for any one surface, 10,000 layers, 4 MiB manifest, 512 MiB per encoded asset. See src/IO/ProjectStore.cpp for validation.

Undo history and viewport are session-only. Opening fits the canvas, restores selection, and starts with clean history. Future editable features must extend the schema and round-trip tests. PNG export is a flattened derivative and does not mark project edits saved.

Image Size adds optional resolution (pixels/inch, 1–9600). Older manifests without it default to 72. This additive field retains version 1 compatibility. Both PNG and JPEG exports include document resolution metadata. Resampling stores the new layer pixels and bounds; undo retains the prior sources only during the current session.

Version 2 adds optional parentID and isGroup on layer records. A group has no image file. Root nodes have no parent; children refer to an existing group. Array order defines bottom-to-top sibling order; renderers traverse each group as a contiguous subtree. Visibility is inherited without changing child flags. Cycles, missing/non-group parents, image-bearing groups, and nesting beyond 64 ancestor levels are rejected. Group ancestors permit room for leaf nodes at the deepest level. Group metadata survives image/canvas resizing and cropping. Older app builds reject version 2 rather than misrender grouped documents. Collapse state is not serialized.

Version 3 adds optional per-layer opacity (finite 0–1) and blendMode (Normal, Multiply, Screen, Overlay, Darken, Lighten, Difference, Color Dodge, Color Burn). Missing fields default to full opacity and Normal. Group records required those defaults until version 8, which lets a folder carry its own opacity; a folder's opacity multiplies into every layer inside it, while its blend mode stays Normal because folders are pass-through. Effects are applied during compositing and retained as metadata when resizing sources. Files declaring older versions cannot contain non-default appearance values.

Version 4 adds optional maskFile and maskEnabled fields to individual layers. Mask filenames must be <layer UUID>.mask.png under images/; enabled defaults to true when a mask exists. Records without masks omit both fields. Groups cannot carry masks in this version. Files declaring versions 1–3 cannot contain mask metadata.

Masks store 8-bit grayscale coverage without alpha (white reveals, black hides). Their normalized extent matches the image’s local rectangle, so the same layer transform applies to both. A uniform 1×1 mask is valid and avoids allocating full-resolution pixels before painting. Nonuniform mask pixels and a thumbnail are immutable assets shared by history. Image Size resamples them with the image transform; Canvas Size and Crop preserve their pixels. Mask pixels count toward their own budget, as large as the image budget; per-side and per-file limits also apply to masks. Disabled masks remain embedded and editable but do not affect compositing. Image-versus-mask target selection is session-only and reopens on image pixels.

Version 5 adds optional maskSourceID: the UUID of a non-group layer supplying live alpha in document coordinates. It multiplies the target’s alpha alongside its enabled raster mask. Source pixels, transform, opacity, raster mask and upstream live masks contribute coverage; visibility and RGB color do not. Sources remain independent layers. Missing references, self-links, cycles, group endpoints and chains over 256 nodes are rejected. Deletion can bake the live coverage into dependent image pixels (retaining their raster masks) or remove the links, as one undoable operation. Links survive image/canvas resize and crop. Older versions default to no live mask; older app builds reject v5.

UI terminology: these alpha links are clipping masks. Alt-click in a row's clipping strip (Option-click on macOS) assigns the lower sibling’s base or releases the connection. Multiple clipped layers share one base, show indented above it, and release when moved outside the contiguous stack. The underlying maskSourceID representation is unchanged.

Version 6 allows maskFile and maskEnabled on group records. A folder has no image, so its mask covers the folder's own transform rectangle (the canvas size when the folder was created); Image Size resamples it through that transform, and Canvas Size and Crop preserve its pixels, exactly as for layer masks. Groups are pass-through, so an enabled folder mask multiplies the coverage of every descendant layer, together with that layer's own mask and any enclosing folders' masks; clipping-mask coverage is unaffected. Files declaring versions 1–5 cannot give a group a mask, and older app builds reject v6.

Version 7 adds adjustment layers: a layer record with an optional adjustment object and no imageFile. Adjustment layers cannot be groups and cannot carry text; like any layer they take a transform, opacity, blend mode, raster mask and clipping link, and they affect everything composited below them. Folders are pass-through, so an adjustment inside a folder also changes what lies below the folder; only a clipping stack gives its members a backdrop of their own. kind is one of Hue/Saturation, Levels, Curves, Exposure, Gradient Map, Grain, Invert, Black & White and Color Balance (version 9 adds three more). Every adjustment, whatever its kind, must carry hue, saturation, lightness, colorize, levels and curves; the other kinds' settings are optional, and a missing one takes that kind's own defaults (not always an identity: Gaussian Blur's radius defaults to 10). The settings: hue (±360), saturation and lightness (±100), colorize and hsvSettings for Hue/Saturation (hsvSettings, when present, wins over the other four); levels (four channel ranges, RGB then red, green, blue); curves (four channel point lists); exposureSettings; gradientMapSettings (shadows, highlights, reversed); grainSettings; blackWhiteSettings; colorBalanceSettings. Out-of-range or non-finite values are rejected. Files declaring versions 1–6 cannot contain adjustment records, and older app builds reject v7. See src/Document/LayerAdjustment.cpp for the exact ranges.

Version 8 lets a folder carry its own opacity, which multiplies into every layer inside it; a folder's blend mode stays Normal because folders are pass-through (files declaring 1–7 require folders at full opacity). It also adds an optional top-level guides array of alignment guides, each with id, axis (horizontal or vertical) and position in document pixels (finite, at most 1,000,000 in magnitude). At most 1,000 guides are stored; files declaring 1–7 cannot contain guides. Guides survive Canvas Size and Crop by offsetting with the canvas.

Version 9 adds three adjustment kinds that sample neighboring pixels: Gaussian Blur (blurRadius, 0.1–250 document pixels), Motion Blur (motionAngle, −90 to 90 degrees, and motionDistance, 1–2000) and Add Noise (noiseAmount, 0.1–400, noiseGaussian, noiseMonochromatic and noiseSeed, so the pattern is stable between sessions). Files declaring 1–8 cannot contain these kinds; the earlier adjustment kinds remain valid at version 7 and up.

Version 10 lets a text layer color some of its letters differently: optional colorRuns in its text metadata (see Editable text). Files declaring 1–9 cannot contain it.

Version 11 lets those letters use different faces too: optional fontRuns in the same metadata. Files declaring 1–10 cannot contain it. colorRuns stays valid from version 10.

Additive layer fields

Later fields are optional and not gated on the version, so older readers ignore them and keep the pixels or the linked mask as they were:

  • maskPlacement and maskLinked: an unlinked mask (maskLinked false; missing means linked) keeps its own transform in maskPlacement, a document-space rectangle like the layer transform, and no longer follows the layer when it moves. maskPlacement requires a maskFile; maskLinked without a mask is accepted and changes nothing.
  • shape: a layer made with the Shape tool keeps its style (kind, red/green/blue, cornerRadius in document pixels, and for lines lineWidth plus start and end as fractions of the layer box) so it redraws cleanly when scaled. Its PNG is still an ordinary raster; once anything else changes those pixels the metadata is dropped.

Editable text

Pixel layer records may include optional text metadata: content, PostScript font name, font size in pixels, RGB color, alignment, tracking, line spacing and optional boxSize paragraph bounds. Text wraps inside these bounds; changing them reflows the text without scaling the font. The PNG remains the display and export fallback. Older readers ignore this metadata. Transforms, duplication, masks, Canvas Size, Crop, Trim and an Image Size that changes only the resolution preserve it; destructive pixel operations, resampling included, rasterize text and omit the metadata on the next save. A missing face is replaced through Qt and fontconfig when edited, which can pick another family, while the saved PNG preserves the original appearance until then. From version 10, optional colorRuns lists letters painted in another color than the text's own red/green/blue: each run has location and length in UTF-16 units of the content, plus red, green and blue (0–1). From version 11, optional fontRuns lists letters set in another face than fontName: the same location and length, plus fontName. Runs of either kind are sorted, do not overlap, have a positive length and end within the content; letters outside every run use the text's color or face.

Layer effects

An optional effects record contains independent stroke, shadow, colorOverlay, innerShadow, outerGlow and innerGlow records. Stroke carries a size (0–500 layer pixels), a color, an opacity and an inside flag choosing which side of the edge it sits on; drop shadow and inner shadow each carry an angle, a distance, a blur, a color and an opacity; color overlay carries a color and an opacity; outer glow and inner glow each carry a size (0–500 layer pixels), a color and an opacity. Each supports optional enabled visibility (missing means visible); hidden effects keep all parameters and remain listed under their layer. Effects, including their visibility, are saved and participate in document undo. They stay with their layer through every edit, pixel edits included; Image Size scales their sizes, distances and blurs with the image. Canvas previews run on a serial background worker with a shared pixel budget; exports render the full-resolution effects. A record omitting an effect means that layer does not have it, so older readers see the effects they understand and ignore the rest.