|
| 1 | +# Canvas Projects — Technical Documentation |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +Canvas Projects provide a save/load mechanism for the entire canvas state. The feature serializes all canvas entities, generation parameters, reference images, and their associated image files into a ZIP-based `.invk` file. On load, it restores the full state, handling image deduplication and re-uploading as needed. |
| 6 | + |
| 7 | +## File Format |
| 8 | + |
| 9 | +The `.invk` file is a standard ZIP archive with the following structure: |
| 10 | + |
| 11 | +``` |
| 12 | +project.invk |
| 13 | +├── manifest.json |
| 14 | +├── canvas_state.json |
| 15 | +├── params.json |
| 16 | +├── ref_images.json |
| 17 | +├── loras.json |
| 18 | +└── images/ |
| 19 | + ├── {image_name_1}.png |
| 20 | + ├── {image_name_2}.png |
| 21 | + └── ... |
| 22 | +``` |
| 23 | + |
| 24 | +### manifest.json |
| 25 | + |
| 26 | +Schema version and metadata. Validated on load with Zod. |
| 27 | + |
| 28 | +```json |
| 29 | +{ |
| 30 | + "version": 1, |
| 31 | + "appVersion": "5.12.0", |
| 32 | + "createdAt": "2026-02-26T12:00:00.000Z", |
| 33 | + "name": "My Canvas Project" |
| 34 | +} |
| 35 | +``` |
| 36 | + |
| 37 | +| Field | Type | Description | |
| 38 | +|---|---|---| |
| 39 | +| `version` | `number` | Schema version, currently `1`. Used for migration logic on load. | |
| 40 | +| `appVersion` | `string` | InvokeAI version that created the file. Informational only. | |
| 41 | +| `createdAt` | `string` | ISO 8601 timestamp. | |
| 42 | +| `name` | `string` | User-provided project name. Also used as the download filename. | |
| 43 | + |
| 44 | +### canvas_state.json |
| 45 | + |
| 46 | +The serialized canvas entity tree. Type: `CanvasProjectState`. |
| 47 | + |
| 48 | +```typescript |
| 49 | +type CanvasProjectState = { |
| 50 | + rasterLayers: CanvasRasterLayerState[]; |
| 51 | + controlLayers: CanvasControlLayerState[]; |
| 52 | + inpaintMasks: CanvasInpaintMaskState[]; |
| 53 | + regionalGuidance: CanvasRegionalGuidanceState[]; |
| 54 | + bbox: CanvasState['bbox']; |
| 55 | + selectedEntityIdentifier: CanvasState['selectedEntityIdentifier']; |
| 56 | + bookmarkedEntityIdentifier: CanvasState['bookmarkedEntityIdentifier']; |
| 57 | +}; |
| 58 | +``` |
| 59 | + |
| 60 | +Each entity contains its full state including all canvas objects (brush lines, eraser lines, rect shapes, images). Image objects reference files by `image_name` which correspond to files in the `images/` folder. |
| 61 | + |
| 62 | +### params.json |
| 63 | + |
| 64 | +The complete generation parameters state (`ParamsState`). Optional on load (older files may not have it). This includes all fields from the params Redux slice: |
| 65 | + |
| 66 | +- Prompts (positive, negative, prompt history) |
| 67 | +- Core generation settings (seed, steps, CFG scale, guidance, scheduler, iterations) |
| 68 | +- Model selections (main model, VAE, FLUX VAE, T5 encoder, CLIP embed models, refiner, Z-Image models, Klein models) |
| 69 | +- Dimensions (width, height, aspect ratio) |
| 70 | +- Img2img strength |
| 71 | +- Infill settings (method, tile size, patchmatch downscale, color) |
| 72 | +- Canvas coherence settings (mode, edge size, min denoise) |
| 73 | +- Refiner parameters (steps, CFG scale, scheduler, aesthetic scores, start) |
| 74 | +- FLUX-specific settings (scheduler, DyPE preset/scale/exponent) |
| 75 | +- Z-Image-specific settings (scheduler, seed variance) |
| 76 | +- Upscale settings (scheduler, CFG scale) |
| 77 | +- Seamless tiling, mask blur, CLIP skip, VAE precision, CPU noise, color compensation |
| 78 | + |
| 79 | +### ref_images.json |
| 80 | + |
| 81 | +Global reference image entities (`RefImageState[]`). These are IP-Adapter / FLUX Redux configs with `CroppableImageWithDims` containing both original and cropped image references. Optional on load. |
| 82 | + |
| 83 | +### loras.json |
| 84 | + |
| 85 | +Array of LoRA configurations (`LoRA[]`). Each entry contains: |
| 86 | + |
| 87 | +```typescript |
| 88 | +type LoRA = { |
| 89 | + id: string; |
| 90 | + isEnabled: boolean; |
| 91 | + model: ModelIdentifierField; |
| 92 | + weight: number; |
| 93 | +}; |
| 94 | +``` |
| 95 | + |
| 96 | +Optional on load. Like models, LoRA identifiers are stored as-is — if a LoRA is not installed when loading, the entry is restored but may not be usable. |
| 97 | + |
| 98 | +### images/ |
| 99 | + |
| 100 | +All image files referenced anywhere in the state. Keyed by their original `image_name`. On save, each image is fetched from the backend via `GET /api/v1/images/i/{name}/full` and stored as-is. |
| 101 | + |
| 102 | +## Key Source Files |
| 103 | + |
| 104 | +| File | Purpose | |
| 105 | +|---|---| |
| 106 | +| `features/controlLayers/util/canvasProjectFile.ts` | Types, constants, image name collection, remapping, existence checking | |
| 107 | +| `features/controlLayers/hooks/useCanvasProjectSave.ts` | Save hook — collects Redux state, fetches images, builds ZIP | |
| 108 | +| `features/controlLayers/hooks/useCanvasProjectLoad.ts` | Load hook — parses ZIP, deduplicates images, dispatches state | |
| 109 | +| `features/controlLayers/components/SaveCanvasProjectDialog.tsx` | Save name dialog + `useSaveCanvasProjectWithDialog` hook | |
| 110 | +| `features/controlLayers/components/LoadCanvasProjectConfirmationAlertDialog.tsx` | Load confirmation dialog + `useLoadCanvasProjectWithDialog` hook | |
| 111 | +| `features/controlLayers/components/Toolbar/CanvasToolbarProjectMenuButton.tsx` | Toolbar dropdown UI | |
| 112 | +| `features/controlLayers/store/canvasSlice.ts` | `canvasProjectRecalled` Redux action | |
| 113 | + |
| 114 | +## Save Flow |
| 115 | + |
| 116 | +1. User clicks "Save Canvas Project" → `SaveCanvasProjectDialog` opens asking for a project name |
| 117 | +2. On confirm, `saveCanvasProject(name)` is called |
| 118 | +3. Read Redux state via selectors: `selectCanvasSlice()`, `selectParamsSlice()`, `selectRefImagesSlice()`, `selectLoRAsSlice()` |
| 119 | +4. Build `CanvasProjectState` from the canvas slice; use `paramsState` directly for params |
| 120 | +5. Walk all entities to collect every `image_name` reference via `collectImageNames()`: |
| 121 | + - `CanvasImageState.image.image_name` in layer/mask objects |
| 122 | + - `CroppableImageWithDims.original.image.image_name` in global ref images |
| 123 | + - `CroppableImageWithDims.crop.image.image_name` in cropped ref images |
| 124 | + - `ImageWithDims.image_name` in regional guidance ref images |
| 125 | +6. Fetch each image from the backend API |
| 126 | +7. Build ZIP with JSZip: add `manifest.json` (including `name`), `canvas_state.json`, `params.json`, `ref_images.json`, and all images into `images/` |
| 127 | +8. Sanitize the name for filesystem use and generate blob, trigger download as `{name}.invk` |
| 128 | + |
| 129 | +## Load Flow |
| 130 | + |
| 131 | +1. User selects `.invk` file → confirmation dialog opens |
| 132 | +2. On confirm, parse ZIP with JSZip |
| 133 | +3. Validate manifest version via Zod schema |
| 134 | +4. Read `canvas_state.json`, `params.json` (optional), `ref_images.json` (optional) |
| 135 | +5. Collect all `image_name` references from the loaded state |
| 136 | +6. **Deduplicate images**: for each referenced image, check if it exists on the server via `getImageDTOSafe(image_name)` |
| 137 | + - Already exists → skip (no upload) |
| 138 | + - Missing → upload from ZIP via `uploadImage()`, record `oldName → newName` mapping |
| 139 | +7. Remap all `image_name` values in the loaded state using the mapping (only for re-uploaded images whose names changed) |
| 140 | +8. Dispatch Redux actions: |
| 141 | + - `canvasProjectRecalled()` — restores all canvas entities, bbox, selected/bookmarked entity |
| 142 | + - `refImagesRecalled()` — restores global reference images |
| 143 | + - `paramsRecalled()` — replaces the entire params state in one action |
| 144 | + - `loraAllDeleted()` + `loraRecalled()` — restores LoRAs |
| 145 | +9. Show success/error toast |
| 146 | + |
| 147 | +## Image Name Collection & Remapping |
| 148 | + |
| 149 | +The `canvasProjectFile.ts` utility provides two parallel sets of functions: |
| 150 | + |
| 151 | +**Collection** (`collectImageNames`): Walks the entire state tree and returns a `Set<string>` of all referenced `image_name` values. This is used by both save (to know which images to fetch) and load (to know which images to check/upload). |
| 152 | + |
| 153 | +**Remapping** (`remapCanvasState`, `remapRefImages`): Deep-clones state objects and replaces `image_name` values using a `Map<string, string>` mapping. Only images that were re-uploaded with a different name are remapped. Images that already existed on the server are left unchanged. |
| 154 | + |
| 155 | +Both walk the same paths through the state tree: |
| 156 | +- Layer/mask objects → `CanvasImageState.image.image_name` |
| 157 | +- Regional guidance ref images → `ImageWithDims.image_name` |
| 158 | +- Global ref images → `CroppableImageWithDims.original.image.image_name` and `.crop.image.image_name` |
| 159 | + |
| 160 | +## Extending the Format |
| 161 | + |
| 162 | +### Adding new optional data (non-breaking) |
| 163 | + |
| 164 | +Add a new JSON file to the ZIP. No version bump needed. |
| 165 | + |
| 166 | +1. **Save**: Add `zip.file('new_data.json', JSON.stringify(data))` in `useCanvasProjectSave.ts` |
| 167 | +2. **Load**: Read with `zip.file('new_data.json')` in `useCanvasProjectLoad.ts` — check for `null` so older project files without it still load |
| 168 | +3. **Dispatch**: Add the appropriate Redux action to restore the data |
| 169 | + |
| 170 | +### Adding new entity types with images |
| 171 | + |
| 172 | +1. Extend `CanvasProjectState` type in `canvasProjectFile.ts` |
| 173 | +2. Add collection logic in `collectImageNames()` to walk the new entity's objects |
| 174 | +3. Add remapping logic in `remapCanvasState()` to update image names |
| 175 | +4. Include the new entity array in both save and load hooks |
| 176 | +5. Handle it in the `canvasProjectRecalled` reducer in `canvasSlice.ts` |
| 177 | + |
| 178 | +### Breaking schema changes |
| 179 | + |
| 180 | +1. Bump `CANVAS_PROJECT_VERSION` in `canvasProjectFile.ts` |
| 181 | +2. Update the Zod manifest schema: `version: z.union([z.literal(1), z.literal(2)])` |
| 182 | +3. Add migration logic in the load hook: check version, transform v1 → v2 before dispatching |
| 183 | + |
| 184 | +## UI Architecture |
| 185 | + |
| 186 | +### Save dialog |
| 187 | + |
| 188 | +The save flow uses a **nanostore atom** (`$isOpen`) to control the `SaveCanvasProjectDialog`: |
| 189 | + |
| 190 | +1. `useSaveCanvasProjectWithDialog()` — returns a callback that sets `$isOpen` to `true` |
| 191 | +2. `SaveCanvasProjectDialog` (singleton in `GlobalModalIsolator`) — renders an `AlertDialog` with a name input |
| 192 | +3. On save → calls `saveCanvasProject(name)` and closes the dialog |
| 193 | +4. On cancel → closes the dialog |
| 194 | + |
| 195 | +### Load dialog |
| 196 | + |
| 197 | +The load flow uses a **nanostore atom** (`$pendingFile`) to decouple the file dialog from the confirmation dialog: |
| 198 | + |
| 199 | +1. `useLoadCanvasProjectWithDialog()` — opens a programmatic file input (`document.createElement('input')`) |
| 200 | +2. On file selection → sets `$pendingFile` atom |
| 201 | +3. `LoadCanvasProjectConfirmationAlertDialog` (singleton in `GlobalModalIsolator`) — subscribes to `$pendingFile` via `useStore()` |
| 202 | +4. On accept → calls `loadCanvasProject(file)` and clears the atom |
| 203 | +5. On cancel → clears the atom |
| 204 | + |
| 205 | +The programmatic file input approach was chosen because the context menu component uses `isLazy: true`, which unmounts the DOM tree when the menu closes — a hidden `<input>` element inside the menu would be destroyed before the file dialog returns. |
0 commit comments