|
EazyDraw Automation API — Current SurfaceStatus: initial implementation, direct-distribution macOS only. For review of structure and naming before extending. Machine-readable contract: Server
VersioningAll resource paths under AuthenticationAll endpoints require a bearer token. Clients supply it in an HTTP header on every request:
Python example
Threat modelThe token protects against:
The token does not protect against:
Endpoints
UUID matching is case-insensitive against Recursive group traversalThe path grammar for descending into nested groups is:
Each The terminal segment is always
|
elementType (from GET /v1/libraries/{L}/elements) | Behavior |
|---|---|
graphic | A copy of the library's embedded graphic is placed on the target layer. |
create-tool | A fresh default graphic of the tool's class is instantiated and placed. Initial size depends on the drawing's current zoom (the existing DKDLibPalette use-action does this). |
arrange-tool | Rejected with 422. Arrange tools don't place graphics. |
attribute-action | Rejected with 422. Attribute-action elements modify a graphic's attributes; they don't add graphics. Use POST .../graphics/{uuid}/attributes (below) to apply the transfer to a target graphic. |
Success — 201 Created. Response body is the standard <graphic> dict shape — index, graphicUUID (freshly generated, never matches the library's source UUID), class, graphicTagString, nameGraphic (set to the library element's nameElement), hiddenBounds, and graphicsCount if the placed graphic is a DKDGroup. The returned graphicUUID is immediately usable for follow-up GET / DELETE / future morph calls.
Position and size. Initial position and size come from the existing DKDLibPalette use-action machinery. For graphic elements, they reflect the library author's design (the graphic's saved bounds). For create-tool elements, they reflect the drawing's zoom state at the time of placement. Neither is predictable in detail — a follow-up position / size / rotation endpoint (planned, modeled on EazyDraw's Morph palette) will let clients place a graphic and then move it precisely.
Errors:
400 Bad Request — missing UUIDs in path, empty body, body is not a JSON object, or missing/empty libraryUUID / elementUUID.401 Unauthorized — bearer token missing or wrong.404 Not Found — drawing, layer, library, or element UUID does not resolve. Body: { "error": "Drawing not found" | "Layer not found" | "Library not found" | "Library element not found" }.422 Unprocessable Entity — element is arrange-tool / attribute-action, or use action produced no graphic for unexpected reasons.500 Internal Server Error — response build failure.Fresh UUIDs. After the graphic is added to the target layer, [newGraphic recurseNewUUIDs] runs to assign fresh graphicUUID values for the new graphic and every descendant (groups, sub-graphics). The library's source UUIDs are never reused — a library can be placed any number of times into one or more drawings and each placement has a distinct identity. (The companion recurseUUID: toggle method, used by the Library and Properties panels, retains its original "assign-if-missing / clear" semantics; recurseNewUUIDs is a new method specifically for force-regeneration after placement.)
Threading. Lookup of doc/layer/lib/element, classification, property-list build, setActiveLayer: save-and-restore, the use-action call (-[DKDDocumentView addGraphicsWithArrayOfPropertyLists:]), and recurseNewUUIDs all run inside one dispatch_sync(dispatch_get_main_queue(), ...) block. The target document's activeLayer is briefly set to the API-supplied target layer for the duration of the add, then restored — this routes the new graphic to the right layer using the existing use-action infrastructure without permanently disturbing UI state.
curl:
curl -i -X POST -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"libraryUUID":"'"$LIB_UUID"'","elementUUID":"'"$EL_UUID"'"}' \
http://localhost:52737/v1/drawings/$DRAWING_UUID/layers/$LAYER_UUID/graphics
Python:
r = session.post(
f"http://localhost:52737/v1/drawings/{drawing_uuid}/layers/{layer_uuid}/graphics",
json={"libraryUUID": lib_uuid, "elementUUID": element_uuid},
)
r.raise_for_status()
new_g = r.json()
print("Placed:", new_g["class"], "uuid:", new_g["graphicUUID"], "bounds:", new_g["hiddenBounds"])
GET /v1/drawings/{D}/layers/{L}/graphics/{G_chain} — fetch a single graphicReturns the standard <graphic> dict for a single graphic identified by UUID chain. Same chain grammar as the morph, export, and DELETE endpoints — one UUID for a top-level layer graphic, additional UUIDs to descend into groups.
Success — 200 OK. Body is the standard <graphic> dict (same shape as one entry in GET /v1/drawings/{D}/layers/{L}/graphics). The index field reflects the graphic's position in its immediate container (layer or parent group), not a global drawing-wide index.
Errors: 400 (missing UUIDs), 401 (auth), 404 (any UUID along the chain does not resolve, or an intermediate non-group blocks descent), 500 (response build failure).
Use case: when you have a UUID from a prior call (POST use, PATCH morph, or a listing) and you want to refetch the current state without re-listing the layer. Cheaper than layer_graphics() for spot-checks; especially useful after operations whose response represents pre-state (DELETE) or in-flight state.
g = ed.graphic(d_uuid, l_uuid, g_uuid)
print(g["class"], g["hiddenBounds"])
DELETE /v1/drawings/{D}/layers/{L}/graphics/{G_chain} — remove a graphicRemoves the graphic identified by UUID chain. The remove is registered with the document's undo manager, so Edit → Undo in the EazyDraw UI restores the graphic at the same z-order index in its original container.
Success — 200 OK. Body is the standard <graphic> dict for the pre-delete state. The graphicUUID will no longer resolve in subsequent calls (until an undo restores it).
Container handling:
chain length == 1): removed via [doc removeGraphic:atIndex:] which handles connection cleanup, contained text pairs, layer panel sync, and undo registration.chain length > 1): removed from its immediate parent group's groupArray via [parentGroup groupRemoveGraphicAtIndex:]. Same undo registration pattern.In both cases the index field in the response dict reflects the position in the immediate container at the time of delete.
Errors: 400 (missing UUIDs), 401 (auth), 404 (any UUID does not resolve, or intermediate non-group blocks descent), 500 (response build failure).
Threading. Resolution, dict build, and removal all run inside one dispatch_sync(dispatch_get_main_queue(), ...) block — atomic against other API requests.
curl -i -X DELETE -H "Authorization: Bearer $TOKEN" \
http://localhost:52737/v1/drawings/$D/layers/$L/graphics/$G
deleted = ed.delete_graphic(d_uuid, l_uuid, g_uuid)
print("Removed", deleted["class"], "at z-order", deleted["index"])
POST /v1/drawings/{D}/layers/{L}/graphics/{G}/attributes — apply a transferApplies a library attribute-action element's DKDTransfer (fill color, line color/style, gradient, hatch, dashes, arrows, brush, shadow, dimension scopes) to a target graphic — the programmatic equivalent of dragging that library swatch onto the graphic, or selecting the graphic and clicking the element's Use button.
Request body:
{
"libraryUUID": "<DKDLib.libraryUUID>",
"elementUUID": "<DKDLibElement.libraryUUID>"
}
Both required, non-empty strings. The element must classify as attribute-action — a DKDLibGraphicElement whose embedded graphic is not a DKDGroup, carries a non-nil dkdTransfer, and has at least one active scope flag (brushScopeTransfer, shadowScopeTransfer, gradientScopeTransfer, hatchScopeTransfer, dashesScopeTransfer, patternScopeTransfer, arrowsScopeTransfer, colorAndStyleScopeTransfer, dimensionScopeTransfer). A graphic / create-tool / arrange-tool element returns 422 (use POST .../graphics to place those).
v1 — top-level graphics only. The target is a single top-level layer graphic; a nested (grouped) UUID chain returns 422. The transfer is applied by the same mechanism as a drag-drop (DKDTransferApply dropTransfer): save the current selection, select the target graphic, run the per-scope inspector-panel applies against it, then restore the prior selection. On-canvas selection must be able to address the target, which today means a top-level graphic. (A brief selection change may be visible to a user watching the document.)
Success — 200 OK. Body is the standard <graphic> dict; the graphicUUID is preserved (the graphic is restyled in place, not replaced). The summary dict does not include attribute values — to see the resulting fill/line/etc. inline, GET .../graphics/{G}/export/native or the layer /state.
Undo. Registered as a single "Transfer" step by applyTransferFromGraphic: (the action name reflects the scopes applied), so Edit → Undo reverts the change.
Locks. Only the layer's layerLock blocks the apply — 423 Locked, with ?force=true to override. The graphic's moveLock / sizeLock / deleteLock do not apply: an attribute change is neither a move, a resize, nor a delete.
Errors:
400 Bad Request — missing path UUIDs; empty/malformed body; missing/empty libraryUUID / elementUUID.401 Unauthorized — bearer token missing or wrong.404 Not Found — drawing, layer, top-level graphic, library, or element UUID does not resolve.422 Unprocessable Entity — element is not an attribute-action (it's a graphic / create-tool / arrange-tool); or the target is a nested graphic (v1 limitation).423 Locked — the target layer is locked; retry with ?force=true.500 Internal Server Error — response build failed.Threading. Resolution, the select → applyTransferFromGraphic: → restore-selection sequence, and the dict build all run inside one dispatch_sync(dispatch_get_main_queue(), …) block — atomic against other API requests.
Python:
g = ed.apply_attributes(d_uuid, l_uuid, g_uuid, lib_uuid, el_uuid)
print(g["class"], g["graphicUUID"])
# to confirm the applied attributes, read the flat state:
native = ed.export_graphic(d_uuid, l_uuid, g_uuid, fmt="native")
POST /v1/drawings/{D}/layers/{L}/graphics/{G_chain}/order — z-orderChanges a graphic's front‑to‑back position within its layer (the layer's graphics array order; 0 = bottom). Reuses -[DKDDocument sendGraphic:toIndex:].
Body: { "to": "front" | "back" | "forward" | "backward" } — front/back to the top/bottom of the layer, forward/backward one step. 200 OK — the <graphic> dict (its index reflects the new z‑position). Undoable; layerLock‑ gated. v1 top‑level graphics (nested → 422).
POST /v1/drawings/{D}/layers/{L}/graphics/{G_chain}/move-to-layer — move to a layerMoves a graphic from its current layer to another, via -[DKDDocument moveGraphic:toLayer:index:] (undo‑aware). Useful for parking a graphic on a working layer or bringing it onto a visible one.
Body: { "layer": "<destination name or uuid>", "index"?: <Number> } — the destination is matched against each layer's uuid (case‑insensitive) then layerName; index is the z‑position in the destination (default: top). 200 OK — the <graphic> dict; graphicUUID is preserved, so after the move address it under the *destination* layer's uuid. Undoable; layerLock on both source and destination is honored (423 + ?force=true). v1 top‑level graphics.
Errors: 400 (UUIDs/body/missing layer), 401, 404 (graphic chain or destination layer unresolved), 422 (nested graphic, or already on that layer), 423 (locked), 500.
POST /v1/drawings/{D}/layers/{L}/graphics/{G_chain}/flip — flip a graphicMirrors a graphic in place — the common vector‑editing flip, and the fix for an upside‑down image (something an AI can spot in a render but EazyDraw itself cannot perceive). Reuses the flip menu command's primitive (flipGraphicsWithFlipSpec:), so it handles the same geometry, grid‑reference, and undo behavior as the UI.
Request body: { "axis": "horizontal" | "vertical" | "mirror" } — horizontal mirrors left↔right, vertical top↔bottom, mirror both (180°).
Success — 200 OK. The standard <graphic> dict; the bounding box is unchanged (a flip preserves position and size), and graphicUUID is preserved. One undoable "Flip …" step. Honors layerLock (423 + ?force=true); a flip is neither a move, resize, nor delete, so the per‑graphic move/size/delete locks do not apply.
Errors: 400 (missing UUIDs / body / bad axis), 401, 404 (chain unresolved), 423 (layer locked), 500.
POST /v1/drawings/{D}/layers/{L}/images — insert content (raster or PDF)Inserts content onto the layer at the front of its z-order (above existing content). This is the *insert into an existing drawing* path; to bring a file in as a new drawing sized to its content, use POST /v1/drawings (see below). The destination is the layer in the URL — make that layer active (the agent drives the destination by choosing the active layer). Page geometry is never changed here.
Request body:
{
"imageBase64": "<bytes>", // OR "path": "~/sig.pdf" (a file the EazyDraw process can read)
"quality": "screen | retina | print", // raster only — resample ceiling 72 / 144 / 300; default retina
"width": <Number>, // raster only — display width in document points; default = native pixel size
"x": <Number>, "y": <Number>, // raster only — lower-left origin; default centered on the visible view
"name": "<string>" // optional — makes the graphic an addressable named slot
}
Provide imageBase64 (the contract for remote/MCP callers; same Base64 the API *returns* image data as) or a local path (tilde‑expanded; read server‑side).
Raster (PNG/JPEG/TIFF/HEIC/…) — routed via NSImage initDKDWithData:. One bitmap graphic at its native pixel size (1 px → 1 pt), matching how opening the image sizes a page; pass width (document points) to scale it down. Height follows the source aspect (never distorted). scaleFract = (targetDPI × displayWidthPoints / 72) / sourcePixelsWide, clamped ≤ 1 (never upsamples); resampled to that fraction and tagged at targetDPI. Centered on the visible view unless x/y given.
201 Created — the <graphic> dict plus an image block:{ "pixelsWide", "pixelsHigh", "dpi", "bytes" }. Undoable "Insert Image".
PDF — detected by NSPDFImageRep. Comes in as vector, one DKDPDFImage graphic per page at native page size (no scaling), all pages sharing one data blob. Pages are laid onto the drawing's existing page grid via pointWithPageNumber: (page *i* → drawing page *i*); pages past the current grid extend below the canvas — growing the page count is the user's call (not done here). width/x/y/quality are ignored. With more than one page the name is suffixed -1, -2, … so each page stays individually addressable; every page also gets its own graphicUUID.
201 Created — { "pageCount": <N>, "graphics": [ <graphic>, … ] },each graphic dict carrying an extra "pageNumber" (0-based). Undoable "Insert PDF".
Both honor the layer's layerLock (423 + ?force=true).
Errors: 400 (missing path UUIDs / body / neither imageBase64 nor path / bad Base64), 401, 404 (drawing/layer not found; or path unreadable), 422 (image data could not be decoded), 423 (layer locked), 500.
PUT /v1/drawings/{D}/layers/{L}/graphics/{G_chain}/image — fill a named slotThe image Fill keystone (the analog of set_text). The target is a named placeholder — a plain rectangle a designer drew where the image goes (no need to author a PNG placeholder), or an existing image. The incoming image is fitted into the placeholder's rectangle preserving aspect (contain: min(rectW/imgW, rectH/imgH)), centered on the placeholder's center, then the placeholder is replaced — the new image keeps the placeholder's nameGraphic and graphicUUID, so the slot stays addressable by the same name for the next fill.
Body is the same as the insert endpoint (imageBase64 or path, plus quality screen/retina/print); resampling/DPI policy is identical, sized to the fitted rectangle. Success 200 OK — the new <graphic> dict + image block. One undoable "Fill Image" step. v1: top-level placeholders only (a nested chain returns 422). Honors layerLock (423 + ?force=true).
Errors: 400 (path/body), 401, 404 (graphic chain unresolved / unreadable path), 422 (nested placeholder, undecodable image, or zero-size placeholder), 423 (locked), 500.
PATCH /v1/drawings/{D}/layers/{L}/graphics/{G_chain} — morph a graphicSets the absolute bounds (position + size) and/or rotation of an existing graphic. The path identifies a single graphic, either at the top of a layer or nested in groups (same UUID-chain grammar as the recursive group-children GET and the graphic-export endpoints).
PATCH /v1/drawings/{D}/layers/{L}/graphics/{G} # top-level
PATCH /v1/drawings/{D}/layers/{L}/graphics/{G1}/graphics/{G2} # nested
PATCH /v1/drawings/{D}/layers/{L}/graphics/{G1}/graphics/{G2}/graphics/{G3} # deeper, and so on
Request body: bounds and/or rotation — at least one required:
{
"bounds": { // optional; if present, all four fields required
"x": <Number>,
"y": <Number>,
"width": <Number, > 0>,
"height": <Number, > 0>
},
"rotation": <Number> // optional; degrees, about the graphic's geometric center
}
x and y are document-space coordinates; width and height must be positive. rotation is in degrees and spins the graphic about its own geometric center (nativeCenterGraphic), matching the Morph panel and the general expectation for grouped/nested content. Omit bounds to rotate in place; omit rotation for a pure resize/move. A custom pivot point (e.g. via each graphic's DKDGridReference snap point, or an arbitrary point) is a future enhancement.
Success — 200 OK. Body is the standard <graphic> dict reflecting the post-morph state: graphicUUID is preserved, but class may change (see "Class swap" below). Note hiddenBounds is the axis-aligned bounding box, so it grows when a graphic is rotated — it is not the requested bounds once rotation is non-zero.
Class swap (minimum geometric generalization). Some EazyDraw graphics cannot represent themselves under a given transform and convert to a more general class, preserving the graphicUUID and z-order index:
DKDCircle asymmetrically scaled converts to DKDOval (acceptsScalingTransform / conversionClassForScaling / convertForScaling).DKDRectangle (which deliberately has no rotation handle) becomes a DKDRotatedRect (acceptsRotationTransform / conversionClassForRotation / convertForRotation, applied via +[DKDTransMorphPanel performRequiredRotationConversionsWithGraphicView:]). Graphics that already carry an angle (DKDTextArea, DKDRotatedRect) rotate in place. Arc/pie-type graphics morph as Bezier geometry about their center.The class field in the response shows the post-swap class. For v1 both swaps are top-level layer graphics only: a nested graphic that would require a class swap returns 422 "Nested graphic class swap not supported in v1" — uniform-scale, pure-translation, and rotation that needs no class change still work for nested graphics.
Transform sequence. Inside one dispatch_sync(dispatch_get_main_queue(), ...), with the target temporarily selected: (1) scale sX = newW/oldW, sY = newH/oldH if not unity; (2) translate (dx, dy) so the box origin lands at the requested x, y; (3) rotate last — setPureRotation + rotateByDegrees:-rotation, applied via [gView morphSelectedGraphicsWithTransform:includeText:YES] which rotates about the graphic's native center. Selection is saved and restored.
Locks. moveLock (when origin changes), sizeLock (when size changes or when rotating — there is no separate rotate-lock in v1), and the layer's layerLock. 423 Locked + ?force=true to override.
Errors:
400 Bad Request — missing path UUIDs; empty/malformed body; neither bounds nor a non-zero rotation; bounds present but missing/non-numeric x/y/width/height or non-positive width/height; rotation not a number.401 Unauthorized — bearer token missing or wrong.404 Not Found — drawing/layer/graphic chain does not resolve.422 Unprocessable Entity — graphic does not accept the scale/rotation and has no conversion class; source graphic has non-positive bounds; nested graphic would require a class swap (v1 limitation); or class conversion produced no replacement.423 Locked — target graphic or its layer is locked against the change; retry with ?force=true.500 Internal Server Error — response build failure.curl:
# resize + reposition
curl -i -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"bounds":{"x":100,"y":100,"width":200,"height":150}}' \
http://localhost:52737/v1/drawings/$D/layers/$L/graphics/$G
# rotate 30 degrees in place
curl -i -X PATCH -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
-d '{"rotation":30}' \
http://localhost:52737/v1/drawings/$D/layers/$L/graphics/$G
Python:
g = ed.morph_graphic(d_uuid, l_uuid, g_uuid,
x=100, y=100, width=200, height=150)
g = ed.morph_graphic(d_uuid, l_uuid, g_uuid, rotation=30) # rotate in place
print(g["class"], g["hiddenBounds"])
# A DKDRectangle rotated comes back class "DKDRotatedRect"; graphicUUID unchanged.
DKDTextAreaProgrammatic control of text-box content. The target is any DKDTextArea graphic, addressed by the same UUID-chain grammar as the morph/GET/DELETE graphic endpoints (top-level or nested in groups).
Model: a DKDTextArea is a first-class DKDGraphic carrying an NSAttributedString. The intended workflow is template-driven: a designer places a styled placeholder text box (font, size, color, alignment, tab stops all set in the EazyDraw UI); the API then feeds plain text into it. Styling is inherited from the placeholder's run-0 attributes — the API does not require the client to specify appearance. Optional uniform overrides adjust the whole box when needed. There are no per-character / per-range style controls in this version (see "Out of scope").
GET .../graphics/{G_chain}/textReturns the current text and live layout metrics.
{
"text": "Quarterly\nSchematic",
"class": "DKDTextArea",
"layout": {
"bounds": { "x": 100, "y": 120, "width": 180, "height": 48 },
"requiredSize": { "width": 142.0, "height": 44.0 }, // size text needs at the current width
"fits": true, // requiredSize.height <= bounds.height
"charCount": 19,
"paragraphCount": 2
},
"base": { // resolved run-0 attributes (the inherited base)
"fontFamily": "Helvetica Neue",
"fontSize": 18.0,
"bold": false,
"italic": false,
"color": "#1A1A1A",
"alignment": "left"
},
"flow": { // text-flow / linked-chain state
"allowTextLink": false, // is the box flow-enabled (the "allow text link" switch)
"linked": false, // part of a linked chain (has a pre or post connector)
"isLead": false, // chain head (no incoming link, has an outgoing one)
"chainLength": 1, // boxes in the chain (1 if not linked)
"chainFits": true // the whole chain holds the text (no box overflows)
}
}
requiredSize is -[DKDTextArea requiredSize:] evaluated at the box's current width — the "give a width, get the needed height" primitive for fitting text to a design area. fits compares that height to the current box height.
flow is the upstream clue that long text can *flow* instead of overflowing: when linked is true, setting text on the lead distributes it across the chain (see Text flow below). chainFits is the chain-wide analog of fits — false means even the whole chain can't hold the text (shrink the font, or enlarge the boxes). allowTextLink mirrors EazyDraw's Text ▸ Allow Text Link switch.
Errors: 400 (missing UUIDs), 401, 404 (chain does not resolve), 422 (the graphic is not a DKDTextArea), 500.
PATCH .../graphics/{G_chain}/textSets text and/or uniform style. Body — all fields optional, but at least one of text or a style override (or autoHeight) must be present:
| Key | Type | Effect | ||||
|---|---|---|---|---|---|---|
text | string | Replace the string (may contain \n and \t). Omit to keep the existing string and only restyle. | ||||
fontFamily | string | Font family, via NSFontManager. | ||||
fontSize | number | Point size (> 0). | ||||
bold | bool | Add/remove the bold trait. | ||||
italic | bool | Add/remove the italic trait. | ||||
alignment | string | left \ | center \ | right \ | justified \ | natural. |
kerning | string | default \ | off \ | tight \ | loose (approximate point-based mapping). | |
color | string | #RRGGBB or #RRGGBBAA. | ||||
autoHeight | bool | When true, grow the box height to fit the text at the current width (setTextAreaSize), as a single undo step with the text change. Ignored when the box flows (a linked chain reflows instead of growing). | ||||
allowTextLink | bool | Set/clear the box's flow switch (-[DKDTextArea allowTextLink], EazyDraw's Text ▸ Allow Text Link). With a linked chain present, enabling it makes set-text reflow across the chain. |
Uniform semantics. The new content is built by taking the placeholder's run-0 attributes, applying the supplied overrides, and laying that uniform style across the entire string. A box with mixed runs is flattened to the run-0 style. This is the Level-1 contract; structural rich text is out of scope.
Tabs / columns. Because the run-0 paragraph style (including tab stops) is inherited, \t-delimited input lays out into the columns the template designer set up — tables, invoices, packing lists work with no table API.
Fit policy. Without autoHeight, the text is set at the current box; if it overflows, the response reports fits:false and requiredSize so the client (or Claude, with a PNG render) decides how to fit — resize the box (PATCH bounds) or resend with a smaller fontSize. EazyDraw never silently auto-fits.
Text flow (linked chains). When a box is flow-enabled (allowTextLink) and linked to following boxes, setting text on the lead makes the whole chain reflow: the text is laid out across the linked boxes (each sized to itself) and each box's slice is written back — attributes preserved (the chain shares one text stream, so styling is not lost the way a manual split would lose it). This reuses the same engine as interactive editing (-[DKDTextArea fullReflowText], gated by reflowApplies = allowTextLink && a post-link connector).
the downstream boxes first, then reflows, so repeated fills don't double-count.
flow.chainFits reports whether the whole chain holds the text(the per-box layout.fits will read true for the lead even when the tail overflows, because after reflow the lead only holds its own slice — use chainFits for the real answer).
boxes when the chain overflows (auto-grow is a planned follow-up). Address the lead box for whole-stream replacement.
DKDTextLinkPath connectors) is done inthe EazyDraw UI today; the API toggles allowTextLink and drives the reflow.
Success — 200 OK. Body is the <text> dict (same shape as GET), reflecting post-change state — so the fit metrics come back in the same call.
Undo. One discrete step named "Set Text" (swapWithUndoDKDContents:); with autoHeight, the height change is grouped into the same step.
Locks. Checked via the standard helper: the layer's layerLock blocks any change; the graphic's sizeLock blocks only when autoHeight would resize. A pure text/style change is not gated by moveLock/sizeLock. 423 Locked with ?force=true to override.
Errors: 400 (missing UUIDs; empty/malformed body; text not a string; nothing actionable supplied), 401, 404, 422 (not a DKDTextArea), 423 (locked), 500.
t = ed.set_text(d, l, g, "Widget\t3\t$9.00\nGrommet\t12\t$1.20", autoHeight=True)
print(t["layout"]["fits"], t["layout"]["requiredSize"])
t = ed.set_text(d, l, g, font_size=10) # restyle only, keep the string
DKDAnnotationA DKDAnnotation is not a graphic; it is owned by a DKDBezier (or subclass), so it is addressed as a sub-resource of its host graphic. Annotation support is plain text only — strings are short and may follow a curve, so there is no size/fit contract; the visual result is confirmed via a PNG export render.
GET .../graphics/{G_chain}/annotation{
"class": "DKDRectangle",
"hasAnnotation": true,
"text": "Section A",
"annotationFormat": "Box", // un-localized DKDAnnotationFormat name
"annotationShow": "Yes" // un-localized DKDAnnotationShow name
}
If the graphic has no annotation (or cannot carry one), hasAnnotation is false and the text/format/show keys are omitted — still 200 OK.
Errors: 400, 401, 404, 500. (No 422: a graphic with no annotation reports hasAnnotation:false rather than erroring.)
PATCH .../graphics/{G_chain}/annotation/textBody: { "text": "<string>" } (required). Sets the annotation's text, inheriting the existing annotation's run-0 attributes, then rebuilds and redisplays it (touchBarSetAnnotationTextWithUndo:). Requires an existing annotation on the target (template-placed); a graphic without one returns 422 "Graphic has no annotation target".
Success — 200 OK. Body is the <annotation> dict (post-change). Undo: one step, "Set Annotation Text". Locks: layerLock only; 423 + ?force=true.
Errors: 400 (missing UUIDs; text missing/not a string), 401, 404, 422 (no annotation target), 423 (locked), 500.
Direct GET of a rendered representation. Path grammar:
/v1/drawings/{D}/export/{format}
/v1/drawings/{D}/layers/{L}/export/{format}
/v1/drawings/{D}/layers/{L}/graphics/{G1}/export/{format}
/v1/drawings/{D}/layers/{L}/graphics/{G1}/graphics/{G2}/export/{format}
/v1/drawings/{D}/layers/{L}/graphics/{G1}/graphics/{G2}/.../graphics/{Gn}/export/{format}
/v1/libraries/{L}/export/{format} (501)
/v1/libraries/{L}/elements/{E}/export/{format} (501)
{format} is one of: native, svg, pdf, png, jpg. Case-insensitive.
format | Content-Type | File extension | Source |
|---|---|---|---|
native | application/json | .ezdjson | DKDFileType_EZDJSON — flat (un-optimized) JSON dictionary. Drawings: full document via [doc documentDictionaryForDocumentIncludeLayers:YES outputFileType:DKDFileType_EZDJSON] with serialization temporarily forced to DKDSerialization_Flat. Graphics: [g propertyListRepresentation], the same dict the file format embeds, JSON-encoded. |
svg | image/svg+xml | .svg | [doc svgDataForDocument]. For graphics, the document's SVG path is invoked while the graphic is temporarily the only selected graphic and exportContents is Graphics_Selected — see "Selection mutation" below. |
pdf | application/pdf | .pdf | Drawings: [doc pdfDataMultiPagination:SinglePagePDFPagination withError:&err]. Graphics: [doc pdfDataWithGraphics:@[g]] (no selection mutation — direct entry point). |
png | image/png | .png | [expCtrlr exportDataWithType:DKDFileType_PNG] with exportContents set per resource (Drawing_Full for drawing/layer, Graphics_Selected with the target selected for graphic). PNG color space, alpha, bits-per-color, and antialias settings come from the document's DKDExport defaults. |
jpg | image/jpeg | .jpg | Same as PNG but DKDFileType_JPG. JPG color space and compression come from DKDExport defaults. |
Content-Type set per the table above.Content-Disposition: attachment; filename="<safe-name>.<ext>" so curl -O, browser saves, and requests's iter_content all get a sensible filename. Filename composition:<displayName>.<ext><displayName>-<layerName>.<ext><displayName>-<graphicNameOrUUID>.<ext>/ \ : * ? " < > |) in the filename component are replaced with _.Drawing_Full export mode at the layer endpoint. The bytes returned for /drawings/{D}/export/{f} and /drawings/{D}/layers/{L}/export/{f} are identical for the same document state. Layer-specific filtering (render only the named layer's graphics on a full-drawing canvas) is a future enhancement and would require temporary layer-state mutation.[gView selectedGraphics]clearSelection, then selectGraphics:@[targetGraphic][exp exportContents], set to ExportContents_Graphics_Selected@finallyThis runs inside dispatch_sync(main_queue, ...) so it is atomic against other API calls. UI selection updates may be visible briefly to a user watching the document. Native and PDF for graphics use direct entry points and do not mutate selection.
/v1/libraries/{uuid}/export/{format} and /v1/libraries/{uuid}/elements/{uuid}/export/{format} return 501 Not Implemented. Implementing this requires a render path that does not assume a host DKDDocument (library elements live under a DKDLib/DKDLibPalette, not a document). Slated for a follow-up.DKDExport defaults. A future revision will accept query-string overrides (e.g. ?dpi=300&colorSpace=srgb).200 — bytes returned with the correct Content-Type.400 Bad Request — {format} is not one of the five recognized values, or (for the recursive graphic endpoint) the UUID chain is empty.404 Not Found — drawing / layer / graphic at the named UUID(s) does not resolve.500 Internal Server Error — export pipeline returned nil bytes.501 Not Implemented — library export endpoints (deferred).PATCH /v1/drawings/{D}/layers/{L} and PATCH /v1/drawings/{D}EazyDraw layer visibility is two‑dimensional: each layer has a LayerState (On / Off / Active, exactly one active = the destination for new content), and the document has a LayerSelect mode governing which non‑active layers show. Export is WYSIWYG — graphics gate their own drawing on visibleLayer:, so what renders to PDF = the active layer + (ON layers, only if layerSelect permits). With one layer this collapses and never matters. This is the basis for conditional regions: park an optional clause on its own layer and toggle it.
PATCH /v1/drawings/{D}/layers/{L} { "layerState": "on" | "off" | "active" }
off hides the layer (drops its content from the render); on shows it;active makes it the destination (the previous active becomes on).
422 "make another layer active first". Setting an already‑active layer active is a no‑op. 200 OK — the updated <layer> dict.
PATCH /v1/drawings/{D} { "layerSelect": "active-only" | "show-others" | "select-others" | "show-all" | "select-all" }
active-only → only the active layer shows; show-others → ON layers visible;select-others → ON layers visible and editable; show-all / select-all → include OFF layers. Set show-others/select-others so a layer you turn ON actually appears in the render. 200 OK — the updated <drawing> dict.
Both are undoable (self‑registering inverse + screen refresh) and mark the document dirty. Layer state is not gated by layerLock (visibility is meta, not a content edit).
POST /v1/drawings/{D}/layers/{L}/order — restack a layerMoves a whole layer in the document's front‑to‑back stack — the order the Layers drawer table shows (front layer at the top) and the order layers paint (front on top of the layers beneath it). This restacks the entire layer; POST .../graphics/{G_chain}/order restacks one graphic *within* its layer.
Request body { "to": "front" | "back" | "forward" | "backward" }
front → top of the Layers table, drawn on top of every layer below it(internal index 0).
back → bottom of the table (internal index count − 1).forward → one step toward the front; backward → one step toward the back.Already at the end → clamped (no‑op move, still 200).
Backed by -[DKDDocument moveLayer:toIndex:], which registers its own undo inverse; the handler wraps it in a discrete undo group, refreshes the screen and the Layers drawer popup (dkdAPIRefreshLayerVisibility), and marks the document dirty. Action name "Layer To Front" / "Layer To Back" / "Layer Forward" / "Layer Backward".
> Front/back vs. the internal index. The vocabulary is viewer‑consistent: > *front* = on top = top of the Layers table. Internally that is array index 0, > and the draw loop iterates the layer array in reverse so index 0 paints last > (on top). Callers never see the index — they say front/back/forward/ > backward.
Not gated by layerLock (restacking is document structure, like layer visibility, not a content edit). 200 OK — the updated <layer> dict.
Errors: 400 body missing or to not one of the four words; 404 drawing or layer not found; 500 response build failed.
POST /v1/drawings/{D}/layers — create a layerAdds a new layer at the front of the stack (top of the Layers table) and, by default, makes it the active layer so the next inserted content lands on it. Mirrors the UI New Layer command (-[DKDLayersDataSource addLayerAction]): the new layer inherits the active layer's dkdScale. Use it to stack content — e.g. create a layer for an incoming PDF backdrop, or the layer a template needs above a PDF for its fill slots.
Request body (optional — send {} or no body for defaults):
{
"name": "<string>", // optional; auto-assigned if omitted, de-duplicated either way
"active": <Bool> // optional; default true — make the new layer active
}
Backed by -[DKDDocument insertLayer:atIndex:] (registers its own undo inverse) at index 0, plus dkdAPISetActiveLayer: when active (self‑registering inverse to the prior active); wrapped in a discrete undo group with a screen + Layers‑drawer refresh and dirty mark. Action name "New Layer". The new layer is assigned a layerUUID so it is immediately addressable. Not gated by layerLock.
Success — 201 Created. Body is the new <layer> dict.
Errors: 400 drawing UUID missing, or body present but not a JSON object; 404 drawing not found; 500 response build failed.
GET .../layer-configurations and apply via PATCH /v1/drawings/{D}A layer configuration (DKDLayersConfiguration) is a saved, named snapshot of a whole layer arrangement — each layer's on/off state, hide-dimensions, lock, and color modification, plus the layer order and the document's layerSelect mode. Users build them in the Layers drawer ("draft", "final", "client-review", …) to switch a drawing between presentations. These endpoints let an agent list them and apply one, so a user can set up and *see* a configuration with AI help.
GET /v1/drawings/{D}/layer-configurations → the saved configs:
[
{
"name": "client-review",
"layerSelect": "show-others",
"layers": [
{ "layerName": "Base", "layerState": "On", "hideDimensions": false, "layerLock": false },
{ "layerName": "Markup", "layerState": "Off", "hideDimensions": true, "layerLock": false }
]
}
]
layerState is the un-localized name (On/Off/Active); layerSelect uses the API vocabulary (show-others, …). The layers order is the configuration's layer order.
PATCH /v1/drawings/{D} { "layerConfiguration": "<name>" } — apply it. This is the HTTP analog of the UI's Load Layer Configuration: it calls -[DKDLayersConfiguration applyToDocument:], setting every layer's state / hide-dimensions / lock / color-mod, reordering the layers, and setting layerSelect. It changes the live document (visible on screen) and is undoable — wrapped in a self-registering helper (dkdAPIApplyLayersConfiguration:) that snapshots the current arrangement as a config, registers the inverse, then applies the new one, so undo and redo both restore the full layout. Action name "Apply Layer Configuration". Render afterward (export png) to *see* the result. 200 OK — the updated <drawing>.
layerSelect and layerConfiguration may be sent together (config applied first); at least one is required. 404 if no saved config has that name. (Saving/deleting configurations is not yet exposed — build them in the UI for now.)
<layer> shape{
"layerName": "<DKDLayer.layerName>",
"layerState": "<nameForLayerState(state, NO)>",
"graphicsCount": <NSUInteger>
}
layerState values are the un-localized names: NotSet, On, Off, Active. graphicsCount is [layer.layerGraphics count].
If a layer has no UUID at the time it is requested, one is assigned via [layer setLayerUUID:] so subsequent calls are stable.
<graphic> shape (entry in /layers/{uuid}/graphics){
"index": <NSUInteger>,
"graphicUUID": "<DKDGraphic.graphicUUID>",
"class": "<NSStringFromClass([g class])>",
"graphicTagString": "<DKDGraphic.graphicTagString>",
"nameGraphic": "<DKDGraphic.nameGraphic>",
"hiddenBounds": { "x": <Number>, "y": <Number>, "width": <Number>, "height": <Number> },
"graphicsCount": <NSUInteger>,
"locks": { "deleteLock": <Bool>, "moveLock": <Bool>, "sizeLock": <Bool>, "layerLock": <Bool> }
}
Array order in /graphics is [layer layerGraphics] order; this is the layer z-order / draw order. index is the position in that array (0 is drawn first, highest index is drawn last / on top within the layer).
nameGraphic is omitted if the graphic has no name set. graphicTagString is the empty string when not set.
graphicsCount is only present when the graphic is a DKDGroup (or subclass thereof), and reports [[g groupArray] count] — the number of immediate children in the group. Non-group graphics omit the key entirely. The presence of the key is itself the "this is a group" signal; in Python, g.get("graphicsCount") returns None for leaf graphics and an integer for groups. An empty group reports 0.
locks is always present. Three flags come from the graphic's DKDLock (deleteLock, moveLock, sizeLock); the fourth (layerLock) is the containing layer's lock. Clients can read this dict before attempting a mutation to know whether it will be blocked — see Lock policy below. (sizeLock is the general-purpose size lock that the API checks. DKDLock also carries DKDLine Pin flags — LockFlag_Lock_Angle, LockFlag_Lock_Length, LockFlag_Lock_Center — which are intentionally not exposed or enforced by the API; see Pin is not an API lock under Lock policy.)
If a graphic has no UUID at the time it is requested, one is assigned via [g setGraphicUUID:] so subsequent calls are stable.
pages/setup, pages/layout, grids, properties, scale, window return the corresponding model object's propertyListRepresentation (or docWindowConfiguratonDictionary for window) verbatim. Keys and value formats match the on-disk drawing file format.
pages is a parent segment for page-related sub-resources; it has no body of its own (no GET handler at /v1/drawings/{uuid}/pages itself yet).
/state — full model state (read backstop)The /state endpoints return an object's complete propertyListRepresentation as JSON — the same dictionary the on-disk file format embeds. They are the backstop: when a structured endpoint doesn't expose a particular attribute, GET the full state and read whatever you need. Keys are EazyDraw on-disk names (PascalCase / mixed), not the camelCase HTTP vocabulary — they are human/AI-readable but not Python-idiomatic.
| Endpoint | Body | Lookup |
|---|---|---|
GET /v1/drawings/{D}/layers/{L}/state | [layer propertyListRepresentation] | 404 Layer not found |
GET /v1/libraries/{L}/state | [lib propertyListRepresentation] | 404 Library not found |
GET /v1/libraries/{L}/elements/{E}/state | [element propertyListRepresentation] | {E} matched case-insensitively against each DKDLibElement.libraryUUID; 404 Library or element not found |
DKDGraphic / DKDGroup / DKDDocument full state is already reachable via GET .../export/native (DKDFileType_EZDJSON), so these three close the remaining gaps (layer, library, library element).
Flat (inline attributes, no hash refs). The dumps resolve attributes inline so a reader never has to chase hash references into an archive store:
propertyListRepresentation serializes graphics with a nil archiveStoreRef, so attributes are already inline — no special handling.DKDLib.propertyListRepresentation builds a hash-ref archive store when its serialization is Optimized. The handler therefore saves the lib's serialization, forces DKDSerialization_Flat, builds the dict, and restores — the same flat-forcing export/native uses on documents.These are GETs: read-only, never blocked by locks, no document mutation.
Binary data → Base64. A library element can carry a button-icon image (DKDImage → NSData), and some graphics embed image data. All API JSON responses funnel through one encoder (_dkd_jsonResponseWithObject:): JSON-safe payloads use NSJSONSerialization unchanged, but a dictionary containing NSData (which NSJSONSerialization rejects) falls back to EazyDraw's jsonDataWithDictionary, which Base64-encodes the bytes — the same representation the on-disk .ezdjson and export/native produce. So image bytes come back as Base64 strings, never an error.
PATCH /v1/drawings/{D}/properties — set document properties> Status: implemented and verified on Version-26 (tested via tools/python/test_properties.py). The API's first non-graphic mutation and the template for read+write on flat (non-hash-optimized) model entities.
Sets document metadata on [doc docProperties]. Merge semantics — only the keys present in the body are changed; omitted keys are left untouched. Companion to GET /v1/drawings/{D}/properties, which returns the same dict shape, so you can read, edit, and send the same dict back.
Request body — any subset of the writable metadata keys. Keys are the verbatim propertyListRepresentation keys (PascalCase, on-disk form), the same vocabulary GET returns. (A clean camelCase external vocabulary with a mapping layer is a known future revision; until then the on-disk keys are the contract.)
| JSON key | JSON type | Model setter |
|---|---|---|
TitleProperty | string | setTitleProperty: |
AuthorsProperty | array of strings | setAuthorsProperty: |
KeywordsProperty | array of strings | setKeywordsProperty: |
DescriptionProperty | string | setDescriptionProperty: |
CommentProperty | string | setCommentProperty: |
OrganizationsProperty | array of strings | setOrganizationsProperty: |
CopyrightProperty | string | setCopyrightProperty: |
ProjectsProperty | array of strings | setProjectsProperty: |
VersionProperty | string | setVersionProperty: |
PATCH /v1/drawings/{D}/properties
{
"TitleProperty": "Quarterly Schematic",
"AuthorsProperty": ["Dave Mattson"],
"CopyrightProperty": "2026"
}
"" (string fields) or [] (array fields).Implementation note — do NOT route through loadPropertyListRepresentation:. The handler must apply changes via the individual setters. DKDProperties.loadPropertyListRepresentation: (DKDProperties.m:418) is a full-replace that resets QuickLook settings and the serialization mode to defaults when their keys are absent — a metadata-only PATCH sent through it would silently clobber QuickLookContent / QuickLookFormat and force Serialization to Flat. Setter-based merge avoids this.
Deferred keys — accepted, ignored in v1. The GET response also carries QuickLookContent, QuickLookPage, QuickLookFormat, QuickLookUseSmallerOf_JPG_PDF, Serialization, and uuid. So a client can GET, edit one metadata field, and PATCH the whole dict back. In v1 these six keys are accepted and ignored (not an error, so round-trips don't break) but not written. They are behavior switches rather than metadata: QuickLook configuration; the Optimized↔Flat Serialization toggle (the on-disk attribute-optimization store); and uuid (DKDProperties.uuidProperties, the yes/no for whether the document assigns persistent UUIDs to graphics and layers). Each warrants its own deliberate design before becoming writable.
Success — 200 OK. Body is the full updated DKDProperties.propertyListRepresentation (same shape as GET), reflecting post-merge state.
Undo / persistence. Mirror the Properties inspector (DKDPropertiesPanel OK path): snapshot the full propertyListRepresentation before the merge, apply via the setters, snapshot it after, then register a symmetric swap on [doc undoManager] with action name "Change Drawing Properties" — undo restores the before-snapshot, redo the after. Registering the undo marks the document dirty (the panel's immediate-apply toggles also call [doc updateChangeCount:NSChangeDone] explicitly; doing both is harmless). The inspector's -swapProperties:oldProperties: lives on the panel and is not reachable headlessly, so the handler needs an equivalent swap target — recommended: a small DKDDocument-level swap method as the prepareWithInvocationTarget: target. That swap applies the full before/after snapshots via loadPropertyListRepresentation: (safe — complete dicts), which is distinct from the partial PATCH apply that uses the individual setters.
Errors:
400 Bad Request — body empty or not a JSON object; a string field given a non-string; an array field given a non-array or an array containing a non-string element; or an unrecognized key (not one of the nine writable keys or the five accepted-and-ignored keys — this guards against PascalCase typos silently no-op'ing).401 Unauthorized — bearer token missing or wrong.404 Not Found — no open drawing with that UUID. Body: { "error": "Drawing not found" }.500 Internal Server Error — response build failed.No lock checks apply — DKDProperties is document metadata, not a graphic or layer, so there is no 423/force path.
Threading. Lookup, setter application, undo registration, and response build run inside one dispatch_sync(dispatch_get_main_queue(), …) block, like the other mutating handlers.
curl:
curl -i -X PATCH -H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"TitleProperty":"Quarterly Schematic","AuthorsProperty":["Dave Mattson"]}' \
http://localhost:52737/v1/drawings/$D/properties
Python:
props = ed.set_properties(d_uuid, {
"TitleProperty": "Quarterly Schematic",
"AuthorsProperty": ["Dave Mattson"],
})
print(props["TitleProperty"])
Every DKDGraphic carries a DKDLock exposing three boolean flags the API enforces (deleteLock, moveLock, sizeLock, plus others not yet wired into the API), and every DKDLayer carries layerLock. These are deliberate user-set protections — set in the EazyDraw UI to keep a graphic or whole layer from being changed accidentally. The API respects them by default and rejects mutations that would violate a lock, returning 423 Locked with a body that explains which lock is in the way:
HTTP/1.1 423 Locked
Content-Type: application/json
{
"error": "Graphic has delete-lock. Retry with ?force=true to override.",
"locks": { "deleteLock": true, "moveLock": false, "sizeLock": false, "layerLock": false }
}
The same locks sub-dict appears in every <graphic> response, so a polite client can read locks before attempting a change and skip the round-trip on known-locked items.
Per-endpoint lock checks:
| Endpoint | Checks |
|---|---|
POST /v1/drawings/{D}/layers/{L}/graphics | destination layer's layerLock |
PATCH .../graphics/{G_chain} (bounds) | moveLock (when origin changes), sizeLock (when size changes), graphic's layerLock |
DELETE .../graphics/{G_chain} | deleteLock, graphic's layerLock |
The PATCH check is granular: a pure translation against a graphic with only sizeLock set is allowed, and a pure resize against a graphic with only moveLock set is allowed. The check fires only when the requested change touches the locked dimension.
Override: all three lock-checking endpoints accept the query parameter ?force=true to bypass. The token is the authorization boundary; force is the explicit "I know what I'm doing" gesture. Use it deliberately in scripts that genuinely need to write through locks (bulk cleanup, migration). The Python client exposes this as a force=False keyword argument:
ed.delete_graphic(d, l, g_uuid) # 423 if delete-locked
ed.delete_graphic(d, l, g_uuid, force=True) # always deletes
ed.morph_graphic(d, l, g_uuid, x=10, y=10, width=20, height=20, force=True)
ed.use_library_element(d, l, lib, el, force=True) # add to a locked layer
GET endpoints are never blocked by locks. Read-only by definition.
Pin is not an API lock. DKDLine (and line-like geometry) supports a UI concept called Pin that constrains interactive endpoint editing along three axes — Angle, Length, and Center — stored on DKDLock as LockFlag_Lock_Angle (0x04), LockFlag_Lock_Length (0x08), and LockFlag_Lock_Center (0x10). These are deliberately not part of API lock enforcement: pinning a line's angle does not make a rotating morph return 423, and pinning its center does not block a move. Pin is an *interactive editing aid* — it shapes how handle drags behave in the EazyDraw UI — not a formal "protect from change" lock like moveLock / sizeLock / deleteLock / layerLock. The API treats Pin as out of scope by design; a script that must respect a line's pin should read it from the graphic's /state and honor it client-side. (Mapping Pin onto API semantics — e.g. should pinned-length veto a non-uniform scale? — is intentionally deferred; it has no clean, unsurprising answer.)
200 — found and serialized (GETs); or, for POST /v1/drawings, the file was already open.201 Created — POST /v1/drawings opened a new window for a file that wasn't already open.400 Bad Request — for the recursive group-children endpoint, the UUID chain was empty. For POST /v1/drawings and POST /v1/libraries, the body was missing/malformed, the path field was missing/empty, or path was not absolute/tilde-prefixed. For DELETE /v1/drawings/{uuid} and DELETE /v1/libraries/{uuid}, missing UUID in the path (defensive — the regex normally guards this).401 Unauthorized — missing Authorization header or token doesn't match the configured value. Response includes WWW-Authenticate: Bearer realm="EazyDraw". Message: Missing or invalid bearer token.404 — no open drawing with that UUID, no layer with the given UUID inside the drawing, no library with that UUID in either _libMenus or open palette windows, or (for the recursive group-children endpoint) some UUID along the chain did not resolve / pointed at a non-group graphic. For POST /v1/drawings and POST /v1/libraries, the supplied path did not exist on disk. Body is GCDWebServer's default error HTML with message Drawing not found, Layer not found, Library not found, or Group not found, or graphic is not a group; for POST endpoints the body is JSON { "error": "...", "path": "..." }.422 Unprocessable Entity — POST /v1/drawings or POST /v1/libraries: file exists but NSDocumentController could not open it, or the opened object was not the expected document subclass. JSON body carries the NSError's localizedDescription. For POST /v1/drawings/{D}/layers/{L}/graphics: library element is arrange-tool or attribute-action, or the use action otherwise produced no graphic. For PATCH /v1/drawings/{D}/layers/{L}/graphics/{G_chain}: graphic does not accept scaling and has no conversion class, source bounds are non-positive, or a nested graphic would require a class swap (v1 limitation).423 Locked — POST /v1/drawings/{D}/layers/{L}/graphics, PATCH .../graphics/{G_chain}, or DELETE .../graphics/{G_chain}: target graphic or its layer is locked against the requested mutation. JSON body: { "error": "...", "locks": { ... } }. Retry with ?force=true to override. See Lock policy above.500 — the response encoder (_dkd_jsonResponseWithObject:) could not produce JSON. Note NSData (e.g. image bytes) is not a 500 — it is Base64-encoded via EazyDraw's jsonDataWithDictionary; a 500 here means even that fallback failed. Message: Failed to serialize JSON response. For POST /v1/drawings / POST /v1/libraries, also: open completion timed out (30s) or response build failed; JSON body.Request handlers run on a background GCD queue. All AppKit / model access is funnelled through dispatch_sync(dispatch_get_main_queue(), …) inside the shared helper _dkd_extractFromDocumentUUID:extractor:, so extractor blocks always run on the main thread and the response is serialized off-main only after the dictionary is built.
These rules apply to all new endpoints, JSON keys, and Key_HTTP_Server_* constants. Match these exactly when extending the API; deviations are legacy compatibility, not patterns to copy.
/v1/drawings/{uuid}/layers/{uuid}, never /Layers/./color-modification, not /colorModification or /color_modification./drawings (collection) ↔ /drawings/{uuid} (one). /layers ↔ /layers/{uuid}. The collection segment never appears in the singular form./v1/drawings/{uuid}/layers/{uuid}/scale, never /v1/drawings/{uuid}/scale-of-layer/{uuid}./v1 for all resource endpoints. /status is intentionally unversioned (liveness probe); if version/build semantics ever change, add /v1/status alongside rather than breaking /status.layerName, layerState, graphicsCount, graphicUUID, graphicTagString, nameGraphic, hiddenBounds, displayName (when newly minted).[g graphicUUID] is "graphicUUID", for [g hiddenBounds] is "hiddenBounds". This makes Key_HTTP_Server_* ↔ getter ↔ JSON key trivially traceable.index, class, status, version, build, drawings, layers, graphics, x, y, width, height."uuid" (lowercase) — the UUID_Key constant, used at every level for the resource's own UUID"DisplayName" (PascalCase) — on the <drawing> and <library> dicts"DocumentFileName" (PascalCase) — the file path, on the <drawing> and <library> dictsNew keys at any level must be camelCase even when they sit alongside these.
Key_HTTP_Server_*)Key_HTTP_Server_graphicUUID = @"graphicUUID").Key_HTTP_Server_drawings, Key_HTTP_Server_status, Key_HTTP_Server_v1 appear in both URL composition and response keys. Multi-word URL segments (color-modification) are inline string literals at the registration site — do not introduce a constant whose JSON-key value would be "color-modification".layers, graphics as URL components when the same word also names a JSON key with the same value) reuse the JSON-key constant. Do not create a parallel Path_* family.{ "x", "y", "width", "height" } with NSNumber doubles. This is the API-facing format. The on-disk propertyListRepresentation may continue to use svgStringWithRectangle strings — that is a serialization detail, not the HTTP contract.NotSet, On, Off, Active. Never localized in API responses.[NSUUID UUIDString]. Matching against client-supplied UUIDs is case-insensitive.NSNumber unsignedInteger); coordinate/dimension values are doubles.The model getter wins for the JSON key (graphicTagString, not a paraphrase) — the goal is debuggability across the stack. If a model name violates camelCase or is awkward (e.g. all-caps acronym), keep the model name and document the exception here rather than diverging silently.
Defined in DKDAppDelegate+HTTP_Server.h:
Key_HTTP_Server_v1 @"v1"
Key_HTTP_Server_drawings @"drawings"
Key_HTTP_Server_libraries @"libraries"
Key_HTTP_Server_inMenu @"inMenu"
Key_HTTP_Server_paletteOpen @"paletteOpen"
Key_HTTP_Server_elementsCount @"elementsCount"
Key_HTTP_Server_elementType @"elementType"
Key_HTTP_Server_nameElement @"nameElement"
Key_HTTP_Server_toolClass @"toolClass"
Key_HTTP_Server_toolName @"toolName"
Key_HTTP_Server_elementType_graphic @"graphic"
Key_HTTP_Server_elementType_create_tool @"create-tool"
Key_HTTP_Server_elementType_arrange_tool @"arrange-tool"
Key_HTTP_Server_elementType_attribute_action @"attribute-action"
Key_HTTP_Server_export @"export"
Key_HTTP_Server_format_native @"native"
Key_HTTP_Server_format_svg @"svg"
Key_HTTP_Server_format_pdf @"pdf"
Key_HTTP_Server_format_png @"png"
Key_HTTP_Server_format_jpg @"jpg"
Key_HTTP_Server_status @"status"
Key_HTTP_Server_AppVersion @"version"
Key_HTTP_Server_AppBuild @"build"
Key_HTTP_Server_displayName @"DisplayName"
Key_HTTP_Server_layerName @"layerName"
Key_HTTP_Server_layerState @"layerState"
Key_HTTP_Server_graphicsCount @"graphicsCount"
Key_HTTP_Server_index @"index"
Key_HTTP_Server_graphicUUID @"graphicUUID"
Key_HTTP_Server_class @"class"
Key_HTTP_Server_graphicTagString @"graphicTagString"
Key_HTTP_Server_nameGraphic @"nameGraphic"
Key_HTTP_Server_hiddenBounds @"hiddenBounds"
Key_HTTP_Server_x @"x"
Key_HTTP_Server_y @"y"
Key_HTTP_Server_width @"width"
Key_HTTP_Server_height @"height"
Key_HTTP_Server_text @"text"
Key_HTTP_Server_fontFamily @"fontFamily"
Key_HTTP_Server_fontSize @"fontSize"
Key_HTTP_Server_bold @"bold"
Key_HTTP_Server_italic @"italic"
Key_HTTP_Server_alignment @"alignment"
Key_HTTP_Server_kerning @"kerning"
Key_HTTP_Server_color @"color"
Key_HTTP_Server_autoHeight @"autoHeight"
Key_HTTP_Server_layout @"layout"
Key_HTTP_Server_base @"base"
Key_HTTP_Server_bounds @"bounds"
Key_HTTP_Server_requiredSize @"requiredSize"
Key_HTTP_Server_fits @"fits"
Key_HTTP_Server_charCount @"charCount"
Key_HTTP_Server_paragraphCount @"paragraphCount"
Key_HTTP_Server_annotation @"annotation"
Key_HTTP_Server_hasAnnotation @"hasAnnotation"
Key_HTTP_Server_annotationFormat @"annotationFormat"
Key_HTTP_Server_annotationShow @"annotationShow"
v1 and drawings are used for both URL composition and as response keys; the others are response keys only.
DKDTextArea. v1 is uniform (Level 1): run-0 attributes + whole-box overrides. If added later, the representation will be Markdown/HTML → NSAttributedString, not character-offset spans.PATCH .../text response already returns requiredSize/fits, which covers the client-driven fit loop. A candidate-string measure (no commit) may come later.POST for open and library-element placement, DELETE for close, PATCH for graphic bounds are implemented — see above)PATCH .../graphics/{chain} rotation is implemented about each graphic's geometric center (nativeCenterGraphic). A caller-supplied pivot (graphic DKDGridReference snap point, bounds corner, or arbitrary document point) is a future enhancement.POST .../graphics/{uuid}/attributes is implemented for top-level layer graphics; a nested target returns 422 (deferred, like the morph nested class-swap limitation)graphicUUID (only the array form is exposed)DKDDocument); 501 in v1DKDExport defaults; query-string overrides deferredPATCH /v1/drawings/{uuid}/properties, proposed). v1 covers the nine metadata fields onlySerialization (Optimized↔Flat) toggle, and the uuid (persistent-UUID yes/no) toggle write — accepted-and-ignored by the properties PATCH; a separate endpoint is deferredproperties (and the other verbatim-plist sub-resources) currently use on-disk PascalCase keys; a clean external camelCase vocabulary with a mapping layer is a planned future revision